# PRD — intersect_cloud (Simpleflow Hub)

Evidence date: 2026-10-12. Source: `git@bitbucket.org:overdrive/intersect_cloud.git`, `main` at `6efd925` (2025-01-22, "fixes update").

There is no prior PRD in Silicon Ark for this slug. The repository README is the FlutterFlow starter text. Requirements below come from the code that implements them. Provenance is marked on each item.

`origin/prod` is a different tree at `f72dda9` (2024-10-24, "update prod arn"). This PRD describes `main` unless a requirement says otherwise. Which branch customers open is **Unknown / Requires Confirmation**. The pipeline maps `main` to the staging deployment and `prod` to the production deployment, and both trees call `https://alb.simpleflowhub.app`.

## Product purpose

Simpleflow Hub is a multi-tenant operations workspace. A signed-in person works inside one tenant: they see that tenant's processes, open the matching forms (job cards, inspections, tasks, bookings, documents, tickets, and customer-specific reports), and admins manage tenants, users, and clients.

Provenance: **Inferred** from the routed screens. The README does not state this. `web/index.html` describes the product as "Simpleflow Process Hub" (**Implemented**).

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Signed-in user | Signs in with email and password, lands on the dashboard, uses Process Hub, tasks, notifications, messages, profile | `lib/pages/auth/login/login_widget.dart`, `lib/main.dart` | Implemented |
| System admin | `FFAppState.systemRole == 'Admin'`. Opens Management (tenants, users, clients). Other system roles are sent from Management to Profile | `lib/pages/management/management/management_widget.dart`, `lib/pages/include/side_nav/side_nav_widget.dart` | Implemented |
| Tenant admin | `FFAppState.tenantRole == 'Admin'`. Side nav opens ManagementUser instead of Management. Tenant-admin pages bounce anyone else to Profile | `side_nav_widget.dart`, `lib/pages/management/management_user/management_user_widget.dart` | Implemented |
| System user / tenant user | Stored values `User`. Labels in the add-user form are "System User" and "Tenant User" | `lib/pages/management/users/add_user/add_user_widget.dart` | Implemented |
| Logged-out visitor | `/` shows Login. Named routes other than Support Ticket List do not set `requireAuth` | `lib/flutter_flow/nav/nav.dart` | Implemented |

Login copies `user.user_role` into `systemRole` and the first `tenant_user_list` row's `role` into `tenantRole`. Provenance: **Implemented** (`login_widget.dart`).

The add-user form stores system role `Admin` when the dropdown shows "System Admin", otherwise `User`. Tenant role options are the stored values `Admin` and `User`, with labels "Tenant Admin" and "Tenant User". Provenance: **Implemented**.

There is no separate documented persona for Pest Elite or Plateau staff. Those names are form packs inside the same app. Whether they are tenants, customers, or both is **Unknown / Requires Confirmation**.

## Main workflows

### W1 — Sign in

Trigger: a person opens `/` while logged out, or opens Login.

Implemented behaviour (`login_widget.dart`, `lib/auth/supabase_auth/email_auth.dart`):

1. Email and password go to `signInWithPassword`.
2. If that returns no user, the button stops.
3. The app loads `tenant_user_list` and `user` for `currentUserUid`.
4. It sets `currentTenantId` and `selectedTenantName` from the first tenant-user row, and writes `current_tenant_id` back onto the `user` row.
5. It copies tenant colours onto `FFAppState` (primary, accent, background, header, tile, menu), with CSS-string defaults `#ff9900`, `#495564`, and `#ffffff` when a colour is missing.
6. It sets `systemRole` from `user.user_role` and `tenantRole` from the first tenant-user row's `role`.
7. It navigates to Dashboard.

The first tenant-user row and the first user row are force-unwrapped. A signed-in auth user with no `tenant_user_list` row or no `user` row will throw. Provenance: **Implemented**. What the screen should do in that case is **Unknown / Requires Confirmation**.

Supabase is initialised with `AuthFlowType.implicit`. Provenance: **Implemented** (`lib/backend/supabase/supabase.dart`).

### W2 — Reset a password

Trigger: Login → "Reset", which pushes `ForgotPassword`.

`supabase_auth_manager.dart` calls `resetPasswordForEmail`. `ResetPassword` is a route that takes a `token` query parameter. The redirect URL configured in the Supabase project is not in this repository. Provenance: **Implemented** for the client call. The hosted redirect is **Unknown / Requires Confirmation**.

### W3 — Create a password with OTP

Trigger: route `createUserPassword`.

The page calls custom action `getOtp`, which runs `signInWithOtp` with `shouldCreateUser: false`, then calls `checkAuth`. Provenance: **Implemented** (`create_user_password_widget.dart`, `lib/custom_code/actions/get_otp.dart`).

`onLoginSuccess` in custom code also calls `updateAppColorsPostLogin` and pushes the Dashboard route by name. That colour helper reads `user_profiles`, a name that has no generated table class under `lib/backend/supabase/database/tables/`. The Login button does not call `onLoginSuccess`; it sets colours itself from `tenant`. Whether FlutterFlow still invokes `onLoginSuccess` is **Unknown / Requires Confirmation**.

### W4 — Stay in a tenant

Trigger: successful login, or a later tenant switch.

`FFAppState` persists `currentTenantId`, `selectedTenantName`, `systemRole`, `tenantRole`, colours, and job-card JSON in `FlutterSecureStorage` under `ff_*` keys. Provenance: **Implemented** (`lib/app_state.dart`).

`UpdateCurrentTenantCall` posts to `rest/v1/rpc/update_current_tenant`. Provenance: **Implemented** (`api_calls.dart`). When the UI calls it, versus only writing `user.current_tenant_id` on login, was not traced through every screen.

System admins have a tenants list. Tenant admins have a tenants-list-user page that refuses anyone whose `tenantRole` is not `Admin`. Provenance: **Implemented**.

### W5 — Open Process Hub

Trigger: the Process Hub tab or route `processHub`.

On load the page:

1. Loads `tenant_process` rows for `currentTenantId`.
2. Loads `process` rows whose ids are in that set, ordered by `order`.
3. Builds chip labels from each process `filter_name`.
4. Loads the current `tenant` row for branding.

When a chip changes, the page deletes `job_card` rows whose `status` is null, then, if the chip is `Job Cards`, calls `JobCardCall` for the current user and tenant, ordered `created_at.desc`, and stores the JSON on `FFAppState.jobCards`.

Lists that have a branch in the widget:

| Chip label | What the branch loads |
|---|---|
| Job Cards | `JobCardCall` |
| Scheduled Inspection | equipment-inspection assign forms (`EquimentInspectionAssignForm`, spelling as in code) |
| Building Inspection | `building_inspection_form` |
| Tasks | tasks list |
| Equipment Inspection | `equipment_inspection_form` |
| Items | items list |
| Bookings | bookings |
| Action Reports | Pest Elite action reports |
| Non-Conformance Reports | Plateau non-conformance |
| Corrective Action Reports | Plateau corrective action |
| Tickets | support tickets |
| Recommendation Reports | Pest Elite recommendation reports |
| Service Reports | Pest Elite service reports and Plateau service reports are separate builders; this chip label is the one compared in the hub |
| Site Forms | Pest Elite site forms |
| Documents | documents |

Chip labels come from the database. A `filter_name` that is not in the table above selects nothing in those branches. Provenance: **Implemented** (`process_hub_widget.dart`). The delete of null-status job cards runs for every chip change, not only Job Cards.

`view_processes` (used by this hub) opens Pest Elite and Plateau create/modals. Provenance: **Implemented**.

### W6 — Phone "More" menu

Trigger: the More tab on a narrow screen (`responsiveVisibility` hides the bottom bar on tablet landscape and desktop).

Items found in `more_nav_bar_widget.dart`: Booking Schedule (`Calendar`), Assets and Parts (`AssetList`), Documents (`DocumentList`), Support Tickets (`SupportTicketList`). Profile and sign-out are further down the same file (sign-out clears `systemRole`). Provenance: **Implemented**.

Side nav on wider layouts includes Messages (with `messagesBadge`), Support Tickets, My Profile, and Management when `systemRole` or `tenantRole` is `Admin`. Provenance: **Implemented** (`side_nav_widget.dart`).

### W7 — Manage users, tenants, and clients

Trigger: a system admin opens Management, or a tenant admin opens ManagementUser.

System-admin pages that redirect when `systemRole != 'Admin'`: Management, add user, edit user, edit user search, tenants add/edit, clients add/edit. Tenant-admin pages that redirect when `tenantRole != 'Admin'`: ManagementUser, user list tenant admin, add user tenant admin, tenants list user, client list user, and the matching edit screens.

New users get `user_role` `Admin` or `User`, an `active` flag from an Active/inactive choice chip, and a tenant role `Admin` or `User`. Provenance: **Implemented** (`add_user_widget.dart`). The Supabase Auth user creation that pairs with the `user` row is **Unknown / Requires Confirmation** beyond this form; the create-password OTP flow refuses to create an auth user (`shouldCreateUser: false`).

### W8 — Produce a PDF

Trigger: a form's PDF action. Custom actions under `lib/custom_code/actions/` build PDFs for tasks, job cards (full and basic), building inspections, equipment inspections, Pest Elite site forms, action reports, recommendation reports (two copies of the action), and Plateau service, non-conformance, and corrective-action reports. HTML placeholder replacers exist for task, support ticket, job card, inspection, and booking. Provenance: **Implemented** as actions. Which button on which form calls which action was not traced for every screen.

### W9 — Send mail

Trigger: a screen calls `SendCustomEmailCall`.

The call posts JSON (`template_name`, `recipient`, `placeholder_values`, `recipient_user_id`, `attachment`, `current_tenant_id`) to `rest/v1/rpc/send_custom_email`. The header map sets `Authorization` twice; in Dart the second value (`Bearer` plus the auth token argument) replaces the first (`Bearer` plus the anon key). The `apikey` header stays the anon key. Provenance: **Implemented**.

A generated table `send_que` has recipient, subject, body, attachment, status, and result. Nothing in the fetched client shows a worker that drains it. The sender is **Unknown / Requires Confirmation** (database trigger, `intersect_lambda`, or something else).

## Functional requirements

### FR-001 — Email sign-in

Requirement: A person can sign in with email and password. On success the app stores the first linked tenant, the system role, the tenant role, and the tenant colours, then opens Dashboard.

Who: any person with a Supabase auth user and a `user` row plus a `tenant_user_list` row.

Trigger: Sign In on Login.

Inputs: email, password.

Expected behaviour: W1.

Outputs: session, `FFAppState` tenant fields, navigation to Dashboard.

Validation: empty result from `signInWithPassword` stops the flow. No other field rules were read off the login validators beyond FlutterFlow's attached validators.

Error conditions: missing tenant-user or user row force-unwraps. Auth errors are whatever `authManager.signInWithEmail` surfaces. The text of that error UI was not extracted.

Dependencies: Supabase Auth at `https://alb.simpleflowhub.app`.

Implementation: Implemented. Confidence: High.

Evidence: `login_widget.dart`, `email_auth.dart`, `supabase.dart`.

### FR-002 — Password reset

Requirement: A person can ask for a reset email and open a reset page that receives a token.

Who: a person who knows the account email.

Trigger: Reset on Login; route `resetPassword`.

Inputs: email; later a `token` route parameter.

Expected behaviour: `resetPasswordForEmail`. The page that consumes the token is routed. The email template and redirect are not in this repo.

Implementation: Implemented on the client. Confidence: Medium. The hosted mail redirect **Requires Confirmation**.

Evidence: `forgot_password` route, `reset_password` route, `supabase_auth_manager.dart`.

### FR-003 — Create password via OTP

Requirement: The create-password page sends an OTP to an existing user and then checks auth. It does not create a new auth user.

Who: a person who already has an auth user.

Trigger: route `createUserPassword`.

Inputs: email.

Expected behaviour: `signInWithOtp` with `shouldCreateUser: false`, then `checkAuth`.

Implementation: Implemented. Confidence: High for the OTP call. Confidence: Medium for the rest of the page's success path.

Evidence: `create_user_password_widget.dart`, `get_otp.dart`.

### FR-004 — Logged-out entry

Requirement: `/` shows Login when there is no session and the shell when there is. Unknown routes do the same. Splash hides after 1 second.

Who: any visitor.

Trigger: app start.

Implementation: Implemented. Confidence: High.

Evidence: `main.dart`, `nav.dart` `errorBuilder` and `_initialize`.

### FR-005 — Router auth gate

Requirement: Only `SupportTicketList` sets `requireAuth: true`. Every other named route uses the default `false`. A logged-out person who opens another named route is not redirected by that flag.

Who: router.

Trigger: navigation.

Implementation: Implemented. Whether that is the intended product rule is **Unknown / Requires Confirmation**. Confidence: High that the code does this.

Evidence: `nav.dart` (`requireAuth` default at the `FFRoute` constructor, only one `requireAuth: true`).

### FR-006 — Tenant theme

Requirement: After login, primary, accent, background, header, tile, and menu colours come from the current `tenant` row. Missing values fall back to the defaults in W1. Colours persist in secure storage.

Who: signed-in user.

Trigger: login, and any later write to those `FFAppState` fields.

Implementation: Implemented. Confidence: High.

Evidence: `login_widget.dart`, `app_state.dart`.

### FR-007 — Shell

Requirement: Phone layouts show a floating bottom bar: Home, Process Hub, Notifications, Tasks, More. Tablet landscape and desktop hide that bar. Side nav covers messages, support, profile, and admin entry.

Who: signed-in user. The shell is also what `/` builds when `loggedIn` is true.

Implementation: Implemented. Confidence: High.

Evidence: `main.dart` `NavBarPage`, `side_nav_widget.dart`, `more_nav_bar_widget.dart`.

### FR-008 — Process list follows the tenant

Requirement: Process Hub chips are the `filter_name` values of processes linked to the current tenant, ordered by `process.order`.

Who: signed-in user with a current tenant.

Trigger: opening Process Hub.

Implementation: Implemented. Confidence: High.

Evidence: `process_hub_widget.dart` init.

### FR-009 — Process lists

Requirement: Choosing a chip shows the list named in W5 when the label matches. Other labels do not match a branch.

Implementation: Implemented. Confidence: High for the labels that were read. Confidence: Medium that every branch was enumerated; the comparisons found are the ones in the table.

Evidence: `process_hub_widget.dart`.

### FR-010 — Null-status job cards are deleted on filter change

Requirement: As implemented, every Process Hub chip change deletes `job_card` rows with `status` null, with no extra tenant filter in that `delete` call.

Who: whoever changes a chip.

This is recorded as behaviour, not as an approved business rule. See `business-rules.md`.

Implementation: Implemented. Confidence: High.

Evidence: `process_hub_widget.dart` choice-chip `onChanged`.

### FR-011 — Pest Elite and Plateau forms

Requirement: Process Hub can open Pest Elite site, action, and recommendation forms, and Plateau service, non-conformance, and corrective-action forms. PDF actions exist for those documents.

Who: a user whose tenant processes expose the matching filter names.

Implementation: Implemented as widgets and PDF actions. Confidence: High that the widgets exist and are imported from `view_processes`. Confidence: Medium on field-level validation inside each form.

Evidence: `lib/pages/pest_elite/`, `lib/pages/plateau/`, `lib/pages/process_hub/view_processes/view_processes_widget.dart`, `lib/custom_code/actions/create_pest_elite_*.dart`, `create_plateau_*.dart`.

### FR-012 — Tasks, assets, bookings, documents, tickets

Requirement: Routed pages exist for tasks (`general_forms/tasks`), assets, calendar bookings, documents, and support tickets. More and side nav open the last four. Tasks is a bottom-nav tab.

Implementation: Implemented. Confidence: High for navigation. Field rules inside each form were not fully extracted.

Evidence: `lib/index.dart`, `nav.dart`, `more_nav_bar_widget.dart`.

### FR-013 — Messages and notifications

Requirement: Message and View Message are routes. Notification is a bottom-nav tab. Messages show a badge from `FFAppState.messagesBadge`.

Implementation: Implemented. How the badge count is calculated was not fully traced. Confidence: Medium.

Evidence: `nav.dart`, `side_nav_widget.dart`.

### FR-014 — Reports

Requirement: A Reports route exists and queries job cards.

Implementation: Implemented as a page. What the report is supposed to decide is **Inferred** from the queries. Confidence: Medium.

Evidence: `lib/pages/reporting/reports/reports_widget.dart`.

### FR-015 — Admin management

Requirement: System admins manage tenants, users, and clients. Tenant admins manage users and clients inside the tenant-admin pages. Each of those pages checks the matching role in `initState` and sends anyone else to Profile.

Implementation: Implemented. The check runs after the first frame, so the page can build once before redirect. Confidence: High.

Evidence: `lib/pages/management/**`.

### FR-016 — Client-side PDF and file upload

Requirement: The app builds PDFs on device and uploads files to Supabase storage buckets `uploads` and `pdfUploads`.

Implementation: Implemented. Confidence: High for the actions. Confidence: Medium for which screens call them.

Evidence: `lib/custom_code/actions/upload_file.dart`, `upload_file_jpg.dart`, `create_*_pdf.dart`, signature pad widgets.

### FR-017 — Custom email RPC

Requirement: The app can post a template name, recipient, placeholders, attachment, and tenant id to `send_custom_email`.

Implementation: Implemented as a client call. The function body is not in this repo. Confidence: High for the call. Confidence: Low for delivery.

Evidence: `SendCustomEmailCall` in `api_calls.dart`.

### FR-018 — Web publish

Requirement: Pushing `main` deploys `./build/web` to S3 with `$AWS_ARN` under the Bitbucket deployment named staging, then invalidates CloudFront `$DISTRIBUTION_ID`. Pushing `prod` does the same with `$AWS_PROD_ARN` under the deployment named production. The build step in the pipeline file is commented out.

Who: Bitbucket Pipelines on those branches.

Implementation: Implemented as pipeline YAML. Whether the variables are set in Bitbucket is **Unknown / Requires Confirmation**. Confidence: High for the YAML.

Evidence: `bitbucket-pipelines.yml`.

## Integrations

| System | Purpose | Direction | Auth | Failure |
|---|---|---|---|---|
| Supabase at `https://alb.simpleflowhub.app` | Auth, PostgREST, storage, RPC | App → API | Anon key in `environment.json`, plus the user bearer token on calls that pass `authToken` | Login stops when sign-in returns null. Other screens mostly check `succeeded` on API calls. Database errors are not given a product-level message in this PRD |
| AWS S3 and CloudFront | Host the web build | Pipeline → AWS | OIDC role ARN from pipeline variables | Not described in the YAML beyond the pipe's own failure |
| Apple sign-in package | Declared | — | — | No page calls it |
| Barcode scanner (FlutterFlow git) | Dependency | — | — | Where it is shown in the UI was not traced |

`user_landing_widget.dart` sets `FFAppState.siteLink` to `https://demo.simpleflowhub.app/`. That page is under `pages/authentication/`, which `lib/index.dart` does not export. Create-task screens contain the literal `https://www.demo.simpleflowhub.app/tasks`. Provenance: **Implemented** strings. Whether those hosts are live is **Unknown / Requires Confirmation**.

## Constraints

- Locale list is English only (`Locale('en', '')`).
- Web `index.html` sets `robots` to `noindex`.
- Android `minSdkVersion` 23, `compileSdkVersion` and `targetSdkVersion` 34. Release signing config in gradle is the debug config.
- Version pinned in pubspec is `1.0.0+1`.
- The anon key is in git (`environment.json`, and also hardcoded on `prod`'s `supabase.dart`). It is not repeated in this PRD.

## Out of scope for this repository

Database policies, the body of every RPC, the AWS account, store releases, and the behaviour of `intersect_cloud_prod`, `intersect_cloud_v2`, and `intersect_lambda`.
