# Technical documentation — flutter_webportal (Datafy Web)

Evidence date: 2026-10-14. Source: `git@bitbucket.org:overdrive/flutter_webportal.git`, branch `main`, commit `b98b9e2` (2026-09-24, `completed update`). Package `datafy_web` `1.0.10+1`.

In-repo companions (richer on some AWS details, stale on others): `docs/PROJECT_DOCUMENTATION.md`, `docs/greenfield-env.md`, `docs/DB_FUNCTION_MIGRATION_MAP.md`, `DEPLOY_AWS.md`, `DEPLOY_FTP.md`, `README.md`. Conflicts with this tree are called out. They were not silently overwritten in the source repo.

## Architecture

Online Flutter web client. It talks to Supabase directly (Auth, PostgREST, Storage, RPCs) and to AWS HTTP APIs for work that the project docs say cannot stay inside self-hosted Postgres.

```text
lib/main.dart
  └─ Supabase.initialize (implicit auth) + dotenv fallback
lib/flutter_flow/nav/nav.dart
  └─ go_router, requireAuth
lib/pages + lib/custom_code/widgets
  └─ screens and manage tabs
lib/custom_code/actions + lib/backend
  └─ RPCs, imports, Portal / Datafy / Bedrock clients
supabase/migrations
  └─ SQL applied out of band (not by the Flutter app)
bitbucket-pipelines.yml
  └─ flutter build web → S3 sync → CloudFront invalidation
```

There is no application server in this repository. There is no background worker process in this repository. Queue processing is Postgres cron plus AWS Lambdas in other stacks (`docs/PROJECT_DOCUMENTATION.md` names `datafy-lambda-new`). Those stacks were not inspected.

## Application structure

| Path | Role |
|---|---|
| `lib/main.dart` | Startup, Supabase init |
| `lib/pages/` | FlutterFlow route screens (dashboard, users, stores, customers, tasks, surveys, promos, kaching, products, reports, auth, profile) |
| `lib/custom_code/actions/` | 117 Dart files including `index.dart` and web/stub duplicates |
| `lib/custom_code/widgets/` | 83 Dart files including `index.dart` and web/stub duplicates |
| `lib/backend/supabase/database/tables/` | 82 generated table/view bindings |
| `lib/backend/api_requests/api_calls.dart` | Large PostgREST surface (`AllApisGroup`) |
| `lib/backend/api_env.dart` | Compile-time API base URLs |
| `lib/backend/portal_functions_client.dart` | JWT POST to Portal SAM, Edge fallback documented |
| `lib/backend/datafy_api_client.dart` | Queue insert then trigger |
| `lib/backend/waterwheel_client.dart` | `waterwheel-control` |
| `lib/auth/supabase_auth/` | Email sign-in, sign-out, password update |
| `lib/flutter_flow/` | FlutterFlow runtime, nav, theme |
| `supabase/migrations/` | 165 `.sql` files, including `_*.sql` helpers |
| `test/` | 9 Dart test files (unit/widget, not a full UI suite) |
| `android/`, `ios/`, `windows/`, `web/` | Flutter platform shells. Product target is web (`README.md`) |
| `docs/` | Scope, feature reference, isolation, greenfield env, DB function map |
| `scope/` | Older task notes and TestSprite scope |

`docs/PROJECT_DOCUMENTATION.md` counts (106 actions, 75 widgets, ~108 SQL files) do not match this tree.

## Languages and frameworks

| Item | Value | Evidence |
|---|---|---|
| Language | Dart | `pubspec.yaml` |
| UI | Flutter, SDK `>=3.0.0 <4.0.0` | `pubspec.yaml` |
| CI Flutter | image `ghcr.io/cirruslabs/flutter:3.41.4` | `bitbucket-pipelines.yml` |
| Generator | FlutterFlow (`FFRoute`, `FFAppState`) | `lib/flutter_flow/` |
| Routing | `go_router` 12.1.3 | `pubspec.yaml`, `nav.dart` |
| State | Provider plus `FFAppState` | `pubspec.yaml`, login widget |
| Backend client | `supabase_flutter` 2.9.0 | `pubspec.yaml` |
| Charts / maps / files | `fl_chart`, `flutter_map`, `excel`, `pdf`, `csv`, `file_picker` | `pubspec.yaml` |
| Local storage | `shared_preferences`, `flutter_secure_storage`, `hive`, `sqflite` | `pubspec.yaml`. Which of these are on the web critical path was not fully traced. |

`publish_to: none`. Private package.

## Routes

Registered in `lib/flutter_flow/nav/nav.dart`. Public: `/` (dashboard if logged in, else login) and `/login`. All others `requireAuth: true`. Unknown location: same dashboard/login split.

| Path | Page |
|---|---|
| `/dashboard` | Dashboard |
| `/tasks` | Tasks |
| `/stores` | Stores |
| `/users` | Users |
| `/customers` | Customers |
| `/products` | Products (`tabIndex`, `productHistory` params) |
| `/nonIssues` | Non-issues |
| `/surveys` | Surveys |
| `/viewSurvey` | `ViewSurveyFormPage` (route name is still `ViewSurveyWidget`) |
| `/viewSurveyResponse` | Survey response |
| `/createSurvey` | Create survey |
| `/promos` | Specials |
| `/kachingNew` | KaChing |
| `/sdc-report` | SDC report |
| `/sjreport` | Sjreport shell (coming soon) |
| `/callsheet` | Empty shell |
| `/notifications` | Activity feed |
| `/profile` | Profile |
| `/baseTemplatePage`, `/baseTemplatePageCopy` | Empty scaffolds |

`/createAccount` is declared on `CreateAccountWidget` and is **not** in the router.

Role gating of the sidebar is in `navbar_widget.dart`, not in `FFRoute`. See `business-rules.md` BR-NAV.

## Auth

- Flow: `AuthFlowType.implicit` (`lib/backend/supabase/supabase.dart`).
- Credentials: `String.fromEnvironment('SUPABASE_URL' | 'SUPABASE_ANON_KEY')`, else `dotenv` from `assets/env/default.env`.
- Login loads `"user"` by auth uid and copies `role` and `super_admin` into app state (`login_widget.dart`).
- Sign-out: `SupaFlow.client.auth.signOut`.
- Password update and email-change confirmation exist on `SupabaseAuthManager`.
- Portal HTTP calls require a session. `invokePortalFunction` throws `StateError` with `not signed in` when there is no JWT (`portal_functions_client.dart`).
- Apple sign-in is a dependency (`sign_in_with_apple`). No product screen was confirmed in the route table. **Unknown** whether it is used.

## Database

Postgres via Supabase. The Flutter app does not migrate itself. Operators apply `supabase/migrations/`.

Themes visible from filenames and changelog (not an exhaustive function list):

- Dashboard RPCs: `get_dashboard_overview`, `get_store_scores`, `get_active_users`, `get_user_statistics`.
- Bulk link: `bulk_link_users_to_stores` and region/channel/code variants.
- KaChing: `link_kachings_to_customer_stores`, `get_customer_linked_stores_page`, `get_customer_store_coverage`, `delete_kaching_campaign`, actor-tracking and export functions.
- Surveys: `get_surveys_for_store`, `get_surveys_for_user`, `get_survey_response_detail`.
- Week reports: `get_combined_week_report_by_store`, `get_combined_week_report_by_region_channel`.
- RLS batch: `20260710120000` family, plus `_rls_enable_one_by_one.sql`, `_rls_disable_one_by_one.sql`, `_validate_rls_policies.sql`.
- Id sequence repairs for `customer_user`, `store_user_link`, `kaching_linked_users`, `kaching_gallery`.

`docs/DB_FUNCTION_MIGRATION_MAP.md` is the in-repo map of which RPCs moved to greenfield. It was not re-audited function by function.

Table bindings include operational tables (`user`, `app_user`, `customer`, `customer_user`, `store`, `store_user_link`, `store_customer_link`, `task`, `survey*`, `promos*`, `kaching*`, `product*`, `sjreport*`, `user_activity`) and names that look like leftovers (`test_table`, `test_tenant`, `task_john`, `store_backup_20241118`, `store_product_backup_20241128`). Whether backup tables are still queried is **Unknown**.

Shared schema with mobile repo `flutter_app` is **Documented** only. That repo was not opened.

## Storage

**Documented** primary bucket `uploads` (`docs/PROJECT_DOCUMENTATION.md`):

| Prefix | Use |
|---|---|
| `uploads/sjreport/` | Sjreport Excel |
| `uploads/promos/`, `uploads/kaching/` | Campaign workbooks |
| `uploads/sdc/` | SDC workbooks |
| `uploads/users/` | User bulk import |

Public URLs are normalized for the greenfield host (`normalizeImageUrl`, named in project docs).

## External APIs

Compile-time defines (`docs/greenfield-env.md`, `bitbucket-pipelines.yml`):

| Define | Used for |
|---|---|
| `SUPABASE_URL`, `SUPABASE_ANON_KEY` | Supabase origin and anon JWT |
| `BEDROCK_ANALYSIS_API_URL` | `POST {base}/analyze-image` with `{ task_id }` |
| `DATAFY_API_URL` | `trigger-read-files`, `trigger-read-promos-kaching` |
| `PORTAL_FUNCTIONS_API_URL` | JWT routes listed below |
| `E2E_SEMANTICS` | Forces Flutter web semantics in CI builds |

Portal slugs called from this repo:

| Slug | Caller |
|---|---|
| `delete-user` | `delete_user_with_linked_items.dart` |
| `process-sdc-upload` | `upload_sdc_workbook.dart` |
| `admin-set-user-phone` | `set_user_auth_phone.dart` |
| `reverse-geocode` | `reverse_geocode.dart` |
| `waterwheel-control` | `waterwheel_client.dart` |
| `trigger-datafy-read-files` | `datafy_api_client.dart` |
| `trigger-datafy-read-promos-kaching` | `datafy_api_client.dart` |

Documented greenfield Supabase origin: `https://supabase.datafy.co.za` (`docs/greenfield-env.md`). Documented Superset origin: `https://superset.waterwheel.datafy.co.za/login/` (`CHANGELOG.md`).

Region cited in deploy examples: `eu-west-1` (`DEPLOY_AWS.md`). The live region is whatever Bitbucket variable `AWS_REGION` is set to. **Unknown** from git alone.

## Infrastructure and deployment

### What the pipeline on this commit does

`bitbucket-pipelines.yml` branches `main` and `AWS-migration` (identical steps):

1. `flutter pub get`
2. `flutter build web --release --no-wasm-dry-run` with `E2E_SEMANTICS`, `SUPABASE_URL_GREENFIELD`, `SUPABASE_ANON_KEY_GREENFIELD`, `BEDROCK_ANALYSIS_API_URL`, `DATAFY_API_URL`, `PORTAL_FUNCTIONS_API_URL`
3. OIDC assume-role (`AWS_ROLE_ARN`, `AWS_REGION`)
4. `aws s3 sync build/web s3://$AWS_S3_BUCKET/ --delete`
5. CloudFront invalidation `/*` when `AWS_CLOUDFRONT_DISTRIBUTION_ID` starts with `E`. If the variable is empty, invalidation is skipped and the step still exits 0.

Documented bucket name: `datafy-prod-webportal` (`DEPLOY_AWS.md`). SPA rule documented there: CloudFront 403 and 404 return `/index.html` with HTTP 200.

### What older docs still say

`README.md`, `DEPLOY_AWS.md`, and `docs/greenfield-env.md` say `main` builds with legacy `SUPABASE_URL` / `SUPABASE_ANON_KEY` and deploys by FTP, and that greenfield defines apply only to `AWS-migration`. The pipeline header comment says FTP variables are for branch `datafy-old`, and that `AWS-migration` is a temporary alias until the default branch swap is verified.

**Requires Confirmation:** treat `bitbucket-pipelines.yml` as the implemented CI contract on this commit, and the FTP pages as documentation that has not been updated.

`DEPLOY_FTP.md` is still in the tree for the FTP path. It was not used by the `main` pipeline steps read here.

### Local run

Documented (`README.md`, `docs/greenfield-env.md`):

```text
flutter run -d chrome --dart-define-from-file=.env
```

or `scripts/run_chrome.ps1`. Supabase-only fallback: `assets/env/default.env`.

TestSprite serve: `npx serve build/web -p 8080 -s` or `build_and_serve.ps1`. `web/serve.json` rewrites SPA routes. Scope file: `scope/TestSprite_Scope.md` (`localPort: 8080`, `needLogin: true`). Latest run notes: `docs/testsprite_run.md` (not re-summarised).

## Environment configuration

Do not commit real keys. `docs/greenfield-env.md` says examples must use `YOUR_API_ID` placeholders. `.env` is local. Bitbucket secured variables hold the anon key.

`assets/environment_values/environment.json` and `assets/env/default.env` are bundled assets. Their contents were not copied into this document so secrets are not duplicated into Silicon Ark. If either file contains a live key, that is a source-repo issue. **Requires Confirmation**.

`ssm_params.json`, `_deploy_only.json`, `_deploy_invoke.json`, and `.cursor_mcp_*.json` sit in the source tree. They were not treated as runtime config and were not copied here.

## Security

Observed, not a pentest:

- Anon key is in the browser bundle by design (`docs/SCOPE.md`).
- Tenant safety is “pass `customer_id`” plus whatever RLS the target database has. Docs disagree on RLS. See business rules.
- Portal routes require the user JWT on the client. Server-side authorization of those Lambdas was not inspected.
- Reverse geocode is capped at 4 in-flight calls with a FIFO queue (`CHANGELOG.md`, `reverse_geocode.dart`).
- Changelog records removal of live API Gateway IDs and a CloudFront export from git. **Requires Confirmation** that no later file reintroduced them.
- `login_widget.dart` sets `selectedCustomer = 1` for every successful login.

No `SECURITY_SETUP.md` on this commit. The 2026-09-28 discovery scan listed that file. It is gone, or it was never on the history this shallow clone can see.

## Background work

Not a Dart worker. Documented database cron: `process_store_queue_smart_timeout` on `store_processing_queue`, schedule `30 seconds` (`README.md`). Task metrics can be queued (`20260712184000_task_metrics_async_via_queue.sql`). Lambda RLS policies for the queue processor: `20260714120000_process_store_queue_lambda_rls.sql`.

Waterwheel start/stop is remote (Portal function), with a one-hour auto-stop **Documented** in the changelog.

## Monitoring

No application APM, Sentry, or log shipper was found in `pubspec.yaml` or the pipeline. The 2026-09-28 discovery scan listed `sonar-project.properties` and `sonarqube_scan.bat`. Neither file is in the tree at `b98b9e2`, and `bitbucket-pipelines.yml` does not run Sonar.

Supabase and CloudFront logs would be outside this repo.

## Development and testing

| Command / asset | Role |
|---|---|
| `flutter test` | Expected runner. Nine files under `test/`: widget smoke, Waterwheel control, survey response display, store sort, campaign id parse, KaChing customer scope, KaChing actor tracking, id-sequence retry, bulk-link RPC result |
| `analysis_options.yaml` | Analyzer options (Flutter lints in `pubspec.yaml` dev dependencies) |
| `scope/TestSprite_Scope.md`, `testsprite_tests/` | Frontend TestSprite plan. Not re-executed for this documentation task |
| `tool/` | One-off Dart scripts (`inspect_xlsx.dart`, `read_xlsx_headers.dart`, `test_rich_vm.dart`) |

Tests were not run during onboarding. This task is documentation of the source tree.

## Platforms checked out but not the product

`android/`, `ios/`, and `windows/` are present. `README.md` says to use Chrome or `web-server`, not the default mobile target. `docs/SCOPE.md` says native mobile apps are out of scope. Launcher icons are still configured for Android, iOS, and web in `pubspec.yaml`.

## Related repositories (named, not inspected)

| Name | Role given in docs |
|---|---|
| `flutter_app` | Mobile offline client; shares migrations |
| `datafy-lambda-new` | SAM: portal functions, datafy import, bedrock, webportal hosting IAM |

## Documentation drift inside the source repo

Recorded so the next sync does not “fix” the code to match a stale doc, or the reverse, without a human:

1. Pipeline vs `README.md` / `DEPLOY_AWS.md` / `docs/greenfield-env.md` on what `main` deploys.
2. `docs/index.md` (“no RLS”) vs `docs/data-isolation.md` / `docs/SCOPE.md` vs the KaChing paragraph in the same isolation doc.
3. `docs/SCOPE.md` SDC audience vs `navbar_widget.dart`.
4. `docs/PROJECT_DOCUMENTATION.md` file counts vs the tree.
5. `SjreportPageTabs` title string “SDC Report” on the Sjreport route.
6. Navbar label `Notfications`.
