# Technical documentation — datafy_web (Datafy Web)

Evidence date: 2026-10-17. Source: `main` at `df8299e` (2026-01-19).

The application repository does not contain an architecture document. This file records the structure that is in the tree. Secrets that are committed (Supabase anon JWT, pipeline variable names) are identified and not copied.

## Stack

| Layer | Choice | Evidence |
|---|---|---|
| UI | Flutter, Material, `useMaterial3: false` | `lib/main.dart` |
| Generator | FlutterFlow. Web meta description is "Built with FlutterFlow." `webviewx_plus` is a FlutterFlow git pin | `web/index.html`, `README.md`, `pubspec.yaml` |
| Language | Dart SDK `>=3.0.0 <4.0.0` | `pubspec.yaml` |
| Package | `datafy_web` version `1.0.10+1`, `publish_to: none` | `pubspec.yaml` |
| Routing | `go_router` 12.1.3, path URL strategy | `lib/main.dart`, `nav.dart` |
| State | `provider` 6.1.5 plus `FFAppState` `ChangeNotifier` | `lib/main.dart`, `lib/app_state.dart` |
| Backend | `supabase_flutter` 2.9.0 (`supabase` 2.7.0) | `pubspec.yaml` |
| Charts and files | `fl_chart` 0.68.0, `excel` 3.x, `pdf` 3.x, `printing` 5.x, `csv` 6.0.0, `file_picker` 10.1.9, `file_saver` | `pubspec.yaml` |
| Local persistence | `flutter_secure_storage` 10.0.0-beta.4, `shared_preferences` 2.5.3, `hive` 2.2.3, `sqflite` 2.3.3+1 | `pubspec.yaml`. App state uses secure storage. Hive and sqflite usage was not traced |
| Lint | `flutter_lints` 4.0.0 | `pubspec.yaml` |
| Icons | `flutter_launcher_icons` 0.13.1, image `assets/images/app_launcher_icon.png` | `pubspec.yaml` |

`dependency_overrides` pins `file` 7.0.1, `http` 1.4.0, `rxdart` 0.27.7, `uuid` 4.x, `web` 1.1.0.

## Application structure

```text
lib/
  main.dart                 app start
  index.dart                barrel of routed widgets
  app_state.dart            persisted session state
  environment_values.dart   loads assets/environment_values/environment.json
  auth/supabase_auth/       email auth manager and user stream
  actions/actions.dart      getStores action block
  backend/supabase/         client, 83 table classes, storage helpers
  backend/api_requests/     AllApisGroup REST wrappers over the same Supabase
  backend/schema/structs/   FlutterFlow structs
  flutter_flow/             theme, nav, upload, generated utilities
  custom_code/actions/      hand-written uploads, exports, RPCs
  custom_code/widgets/      dashboard and export widgets
  pages/                    feature screens (several parallel trees)
  create_survey/            routed survey builder
  view_survey/              routed survey viewer
  view_survey_response/     routed response viewer
android/                    applicationId coza.datafy.web
ios/                        iOS runner
web/                        index.html, Flutter web shell
windows/                    Windows runner
assets/                     fonts, images, videos, audios, rive, pdfs, jsons, environment_values
test/widget_test.dart       default smoke pump
tests/selenium/datafy_web.side
build/web/                  tracked web build output used by the deploy step
bitbucket-pipelines.yml
sonar-project.properties
```

`analysis_options.yaml` is at the repository root (listed at task creation and present as a Flutter project file).

Screen code is FlutterFlow output: large widget files, a `*_model.dart` beside each widget, and repeated layout. Custom behaviour that is safe to treat as intentional lives mainly under `lib/custom_code/` and in the login and navbar callbacks.

## Runtime startup

`main` does the following, in order (`lib/main.dart`):

1. `WidgetsFlutterBinding.ensureInitialized`.
2. `GoRouter.optionURLReflectsImperativeAPIs = true`.
3. `usePathUrlStrategy`.
4. `FFDevEnvironmentValues.initialize`, which JSON-decodes `assets/environment_values/environment.json` and reads `version`. On failure it prints and leaves version empty.
5. `SupaFlow.initialize`.
6. `FlutterFlowTheme.initialize`.
7. `FFAppState.initializePersistedState`.
8. `runApp` with `ChangeNotifierProvider`.

`MyApp` builds the router from `createRouter`, listens to `datafyWebSupabaseUserStream`, and listens to `jwtTokenStream`. After 1 second it hides the splash. The splash asset is `assets/images/Logo-white.png`.

Environment constant `currentEnvironment` is `Production`. The JSON body is:

```json
{ "version": "5.31.7" }
```

That version is not the pubspec version `1.0.10+1`. What `5.31.7` identifies (FlutterFlow project version or app release) is **Unknown / Requires Confirmation**.

## Routing

`createRouter` in `lib/flutter_flow/nav/nav.dart`.

| Name | Path |
|---|---|
| `_initialize` | `/` (login if logged out, else dashboard) |
| Login | `/login` |
| CreateAccount | `/createAccount` |
| Dashboard | `/dashboard` |
| BaseTemplatePage | `/baseTemplatePage` |
| Tasks | `/tasks` |
| Customers | `/customers` |
| Users | `/users` |
| Stores | `/stores` |
| Products | `/products` (`tabIndex`, `productHistory`) |
| Surveys | `/surveys` |
| Notifications | `/notifications` |
| Sjreport | `/sjreport` |
| Callsheet | `/callsheet` |
| Profile | `/profile` |
| Promos | `/promos` |
| KachingNew | `/kachingNew` |
| viewSurvey | `/viewSurvey` |
| NonIssues | `/nonIssues` |
| BaseTemplatePageCopy | `/baseTemplatePageCopy` |
| viewSurveyResponse | `/viewSurveyResponse` |
| createSurvey | `/createSurvey` |

`FFRoute.requireAuth` defaults to false and no route overrides it. The redirect that would send an anonymous user to `/login` therefore does not run. The error builder shows the dashboard when `loggedIn` is true and the login widget otherwise.

Initial location is `/`.

## Authentication

`SupaFlow.initialize` (`lib/backend/supabase/supabase.dart`):

- URL: `https://datafy.appmonitor.co.za`.
- Header `X-Client-Info: flutterflow`.
- Anon key: a JWT committed in that file. Not repeated here. The payload's role is the Supabase `anon` role (standard client key). Expiry encoded in that token runs to 2035. Treat the file as containing a credential.
- `debug: false`.
- `FlutterAuthClientOptions(authFlowType: AuthFlowType.implicit)`.

`SupabaseAuthManager` implements sign-out, delete user, update email, reset password for email, email sign-in (via `EmailSignInManager`), and Apple sign-in. Login uses email sign-in only.

After sign-in, `login_widget.dart` queries `UserTable` with `eqOrNull('id', currentUserUid)`.

Auth user stream type is `datafyWebSupabaseUserStream` (`lib/auth/supabase_auth/supabase_user_provider.dart`).

## Data

Schema SQL is not in the repository. The client binds these tables and views (name as in `tableName`):

`app_user`, `archive_settings`, `auth_user_view`, `brand`, `callsheet`, `category_images`, `channel_data`, `client`, `combined_week_report`, `customer`, `customer_brand`, `customer_client`, `customer_store`, `customer_user`, `file`, `file_upload`, `john_data`, `kaching`, `kaching_gallery`, `kaching_linked_users`, `kaching_uploads`, `linked_issue`, `log`, `logs`, `main_issue`, `model_versions`, `notification`, `producy_view`, `product`, `product_full_list`, `product_grouped`, `product_images`, `product_ml`, `product_store`, `product_store_processing_queue`, `promo_gallery`, `promo_kaching_uploads`, `promo_uploads`, `promos`, `promos_linked_users`, `queue_processing_log`, `rep_list`, `sjreport`, `sjreport_archive`, `sjreport_spar`, `sjreport_summary`, `sjreport_uploads`, `sites`, `store`, `store_backup_20241118`, `store_customer_link`, `store_processing_queue`, `store_product`, `store_product_backup_20241128`, `store_promo`, `store_updates_log`, `store_user_flattened`, `store_user_link`, `store_view`, `store_view_admin`, `store_view_all`, `store_view_new`, `store_visibility`, `survey`, `survey_multiple_choice`, `survey_questions`, `survey_responses`, `survey_store_link`, `task`, `task_file`, `task_john`, `tasks_create_logs`, `temp_products`, `test_table`, `test_tenant`, `user`, `user_activity`, `user_email_view`, `user_sign_in_info`, `user_store`, `user_view`, `version`, `view_store_user_link`.

`AllApisGroup.getBaseUrl()` is `https://datafy.appmonitor.co.za/rest/v1`. Call classes cover stores (admin, linked, region, channel), store tasks and counts, products and SKUs, product ML, SJ report and count, logs and notifications, linked and all promos, KaChings, and surveys, survey responses, user activity, SDC report, visibility, and `AddStoresToQue`.

RPCs called from `lib/custom_code/`:

| RPC | Caller |
|---|---|
| `get_dashboard_overview` | `get_dashboard_overview.dart` |
| `get_store_scores` | `get_store_scores.dart` |
| `get_active_users` | `get_active_users.dart`, active-user widgets |
| `get_last_user_activity` | `get_last_user_activity.dart` |
| `get_combined_week_report_by_store` | `get_combined_week_report_by_store.dart` |
| `get_combined_week_report_by_region_channel` | `get_combined_week_report_by_region_channel.dart` |
| `get_user_stores_for_report` | `get_user_stores_for_report.dart` |
| `update_user_activity` | `update_user_activity.dart` |
| `export_tasks_to_file` | `task_export_widget.dart` |
| `get_tasks_for_store_latest_week` | `export_tasks_to_excel.dart` |
| `get_active_kachings_with_activity` | `export_active_kachings_to_excel.dart` |

## Storage

`lib/backend/supabase/storage/storage.dart` uploads bytes with `uploadBinary` and returns a public URL. Delete splits a public URL into bucket and path. Call sites pass bucket name `uploads` (user edit, admin edit, task edit, store add, site task add/edit).

A hardcoded public object on a different host is used as the empty profile image: `https://alb.datafyapp.co.za/storage/v1/object/public/uploads/R%20(1).png`. That host is not the Supabase URL in `supabase.dart`. **Unknown / Requires Confirmation** whether `alb.datafyapp.co.za` is an older project, a custom domain, or a stale asset.

## External HTTP

`analyzeImageApi` (`lib/custom_code/actions/analyze_image_api.dart`):

- `POST https://i78eyrnc87.execute-api.eu-west-1.amazonaws.com/Stage/analyze-image`
- Body: `{ "image_id": "<id>" }`
- Header: `Content-Type: application/json` only
- Called from `lib/pages/tasks/tasks/tasks_widget.dart`

The path segment `Stage` is the API Gateway stage name in the URL. It is not a comment. Whether that stage is the live model endpoint is **Unknown / Requires Confirmation**.

`product_ml` and `model_versions` are bound tables. Their relationship to this HTTP call is **Inferred**.

## Platforms

| Target | Evidence | Shipping status |
|---|---|---|
| Web | `web/`, `build/web/`, pipeline `LOCAL_PATH: './build/web'` | Intended deploy artifact. Deploy step is commented out |
| Android | `android/app/build.gradle` applicationId `coza.datafy.web` | Project present. Release process **Unknown** |
| iOS | `ios/` | Project present. Bundle shipping **Unknown** |
| Windows | `windows/` | Project present. Shipping **Unknown** |

`web/index.html` sets `robots` to `noindex`, apple web app title `Datafy Web`, and a Flutter base href of `$FLUTTER_BASE_HREF`.

## Deployment and CI

File: `bitbucket-pipelines.yml`. Image: `atlassian/default-image:3`.

| Pipeline | Steps that are not commented |
|---|---|
| `default` | SonarQube |
| `branches.main` | SonarQube. Deploy step exists and is commented |
| `pull-requests.**` | Install dependencies (git, python, JDK, selenium, bandit, safety), SonarQube, Safety, Gitleaks 8.15.3, OWASP Dependency-Check 7.4.4, Selenium, prepare results, POST to Supabase `deploy_results` |
| `custom.manual-security-scan` | Same as the pull-request pipeline |

Sonar pipe `sonarsource/sonarqube-scan:2.0.1`. Host and token are `$SONAR_HOST_URL` and `$SONAR_TOKEN`. `sonar-project.properties` sets:

- `sonar.projectKey=overdrive_datafy_web_d647d0ad-8efb-43dd-9f4e-35f28942bc1d`
- `sonar.host.url=https://scan.appmonitor.co.za`

Deploy step (commented), OIDC, deployment name `Production`:

- `atlassian/aws-s3-deploy:1.1.0` with `$AWS_ARN`, `$AWS_REGION`, `$AWS_S3_BUCKET`, local path `./build/web`.
- `atlassian/aws-cloudfront-invalidate:0.10.1` with `$DISTRIBUTION_ID`.

Scan-result POST (pull requests): `https://supabase.appmonitor.co.za/rest/v1/deploy_results` using `$SUPABASE_API_KEY`. This is a different Supabase project from the app.

Resend (`https://api.resend.com/emails`, from address `security-scanner@yourdomain.com`) and Slack (`$SLACK_WEBHOOK_URL`) steps are commented. The from-address is a placeholder in the pipeline file.

The Selenium step looks for `tests/selenium/*.side`. `tests/selenium/datafy_web.side` is in the repository. The Python invocation is `python3 -m selenium.webdriver.support.event_firing_webdriver`, which is not a Side-file runner. **Inferred:** that step does not execute the Side file as a real browser suite. **Requires Confirmation** if a later image or wrapper makes it work.

Safety runs only when `requirements.txt` exists. No `requirements.txt` was found. The step writes a skip message.

Bandit is installed in the dependency step and the Bandit step itself is commented. Bandit is a Python scanner; this repository is Dart.

`bitbucket-pipelines copy.yml` and `bitbucket-pipelines_old.yml` were listed at task creation. They were not used as the current pipeline. If they differ, the file that runs is `bitbucket-pipelines.yml`.

## Testing

| Asset | What it does |
|---|---|
| `test/widget_test.dart` | Pumps `MyApp`. The test name still says "Counter increments smoke test". No expectations |
| `tests/selenium/datafy_web.side` | Selenium IDE side file. Not executed by a normal Flutter test run |
| `sonarqube_scan.bat`, `commit.bat` | Present at the root in the task-creation listing. Local helper scripts. Not a test suite |

No `integration_test/` directory was found.

## Security notes (observation, not a pentest)

- The Supabase anon key is in source. That matches Supabase's public client key model only if the project relies on RLS. RLS is not in this repository, so the protection level is **Unknown / Requires Confirmation**.
- Analyze-image is called with no client credential.
- Storage helper requests public URLs for the `uploads` bucket.
- `web/index.html` is `noindex`.
- Pipeline gitleaks and dependency-check run on pull requests, not on `main`.
- This document does not copy tokens, anon keys, or the contents of `environment.json` beyond the version field (the file has no other keys).

## Development process

`README.md` tells the reader to use the Flutter stable channel. There is no documented setup for the Supabase project, no `.env` example, and no Makefile. Configuration that the app needs at runtime is compiled into `supabase.dart` and `environment.json`.

FlutterFlow markers ("DO NOT REMOVE OR MODIFY THE CODE ABOVE") remain in custom actions. **Inferred:** custom actions are written in FlutterFlow's custom-code editor or pasted under that header. Whether the team still exports from FlutterFlow is **Unknown / Requires Confirmation**.

## Monitoring

No application monitoring SDK (Sentry, Crashlytics, or similar) was seen in `pubspec.yaml`. Pipeline scan results can be posted to `deploy_results` on the app-monitor Supabase project. SonarQube is the static analysis host. User activity inside the product is the RPC `update_user_activity`, which is product data, not ops monitoring.

## Git

- Remote: `git@bitbucket.org:overdrive/datafy_web.git`.
- Branch: `main` only.
- First commit: `7c71293` on 2024-04-22, "Initial commit".
- Tip: `df8299e` on 2026-01-19.
- 52 commits, 774 tracked paths.

History between the initial commit and the tip is mostly short subjects ("update web", "update pipeline", "latest update"). It is a poor record of product intent.
