# Technical documentation — flutter_app (Datafy Ai)

Evidence date: 2026-10-15. Source: `https://bitbucket.org/overdrive/flutter_app.git`, `develop` at `7d303449744ed14e8e5777c840c695bc37427c5f` (2026-04-13). Version in `pubspec.yaml`: `1.0.79+100103`.

OVER-119 recorded `75ce743` (2026-02-06). That commit is in history and is not `HEAD`.

## What this repository is

A Flutter client named Datafy Ai (package `datafy_ai`), generated in the FlutterFlow style (`lib/flutter_flow/`, page widget/model pairs, `FFAppState`, `FFRoute`). It uses Supabase for auth and online data, and PowerSync for Rep offline data.

It is not the system that creates the retail data. Generated table classes exist for upload and report tables. Their writers are outside this repo.

## Application structure

```text
lib/
  main.dart                 app start, locales, role refresh
  app_constants.dart        PowerSync URL, local schema JSON, storage bucket, date formats
  environment_values.dart   reads assets/environment_values/environment.json
  auth/                     Supabase auth manager
  backend/supabase/         client init, generated table classes
  backend/schema/           structs and SyncStatus enum
  flutter_flow/             nav, theme, custom_functions.dart
  pages/rep/                Rep screens
  pages/admin/              Admin screens
  pages/includes/           login, verify, profile, notifications
  services/powersync/       init, query, write, actions
  services/force_update_service.dart
  widgets/force_update_overlay.dart
android/                    applicationId coza.datafy.web, Fastlane production
ios/                        bundle id coza.datafy.web, Fastlane beta (TestFlight)
web/                        Flutter web tree. No web deploy step in the pipeline
supabase/migrations/       three SQL files only
dependencies/ff_commons, ff_theme
scripts/bump_version.sh, test-pipeline-local.ps1
sync_rules.yaml
```

About 373 Dart files under `lib/`. Route table: `lib/flutter_flow/nav/nav.dart` (`go_router` 12.1.3).

Entry: `main` → `SupaFlow.initialize` → persisted `FFAppState` → `MyApp` with `MaterialApp.router`. Title `Datafy Ai`. Locales `en`, `af`. Path URL strategy is on (`usePathUrlStrategy`).

## Frameworks and languages

| Item | Value | Evidence |
|---|---|---|
| Language | Dart | `pubspec.yaml` |
| UI | Flutter, Material | `uses-material-design: true` |
| SDK | `>=3.0.0 <4.0.0` | `pubspec.yaml` |
| CI image | `ghcr.io/cirruslabs/flutter:stable` | `bitbucket-pipelines.yml` |
| State | `provider` 6.1.5 | `pubspec.yaml`, `main.dart` |
| Router | `go_router` 12.1.3 | `pubspec.yaml` |
| Local store | PowerSync 1.14.1, `sqflite` 2.3.3+1, `hive` 2.2.3 | `pubspec.yaml` |
| Android | compileSdk 35, targetSdk 35, minSdk `flutter.minSdkVersion`, Java 8, namespace `coza.datafy.web` | `android/app/build.gradle` |
| iOS display | CFBundleDisplayName `Datafy`, CFBundleName `Datafy Ai`, URL scheme `datafyai` | `ios/Runner/Info.plist` |
| Android label | `Datafy` | `AndroidManifest.xml` |

`publish_to: 'none'`. The package is not published to pub.dev.

Path dependencies: `./dependencies/ff_commons` and `./dependencies/ff_theme`. A comment says `1.0.54+84`. CI overwrites their `version:` line with the app version.

## Environments and configuration

| Input | Where | Notes |
|---|---|---|
| `SUPABASE_URL` | `--dart-define`, default `https://datafy.appmonitor.co.za` | `lib/backend/supabase/supabase.dart` |
| `SUPABASE_ANON_KEY` | `--dart-define`, default compiled into that file | Default is a JWT. Not copied into Silicon Ark. Review whether it belongs in git |
| `SUPABASE_SERVICE_ROLE_KEY` | Bitbucket secured variable | Used only by the pipeline PATCH. Not in the app binary from this file |
| `SUPABASE_PROJECT_URL` | Optional pipeline override | README: custom domain may not accept REST PATCH |
| `assets/environment_values/environment.json` | `appVersion` `1.0.38` | Loaded at startup. No other Dart reader of `.appVersion` was found |
| Android signing | `android/key.properties` locally, Bitbucket variables in CI | `key.properties` is gitignored per README |
| Play JSON | `ANDROID_PLAYSTORE_JSON_BASE64` | Decoded in CI to `android/playstore-credentials.json` |
| iOS API key | `IOS_APP_STORE_CONNECT_*` | Custom pipeline |
| Match (optional) | `MATCH_GIT_URL`, `MATCH_GIT_BASIC_AUTH`, `MATCH_PASSWORD` | iOS Fastfile, readonly appstore match |

CI build passes the URL and anon key as dart-defines:

`flutter build appbundle --release --build-name=$VERSION_NAME --build-number=$VERSION_CODE --dart-define=SUPABASE_URL=... --dart-define=SUPABASE_ANON_KEY=...`

Auth options: `AuthFlowType.implicit`, header `X-Client-Info: flutterflow`, `debug: false`.

## Auth

`lib/auth/supabase_auth/`. Login UI calls `authManager.signInWithEmail` only.

Phone mode: `phoneToEmail` → `{digits}@datafy.co.za`.

Role is not in the JWT handling shown in `main.dart`. It is a column on `app_user`, loaded after login and again on session restore.

`signInWithApple` is declared on `AuthManager` and is not called from `login_widget.dart`.

Router: `requireAdmin` on the 22 admin routes. `requireAuth` is unused (always false).

## Data

### Supabase

Client: `supabase_flutter` 2.9.0, plus `postgrest` 2.4.2, `gotrue` 2.12.0, `storage_client` 2.4.0, `realtime_client` 2.5.0.

Generated tables live in `lib/backend/supabase/database/tables/` (82 files). Many are views, queues, or backups. Screens use a subset: `app_user`, `customer_user`, `store_user_link`, `task`, `product_store`, `product_images`, `store_promo`, `promos_linked_users`, `promo_gallery`, `kaching_linked_users`, `kaching_gallery`, `survey`, `survey_questions`, `survey_multiple_choice`, `survey_store_link`, `survey_responses`, `user_activity`, `version`, `category_images`.

Migrations committed here:

| File | Change |
|---|---|
| `20250212000000_add_photo_id_to_promo_kaching.sql` | Photo id columns for promo and KaChing attachment sync |
| `20250218000000_add_region_channel_to_survey_store_link.sql` | `region` and `channel` on `survey_store_link` |
| `20250223000000_channel_data_dischem.sql` | Insert `DISCHEM` into `channel_data` if missing, with a logo URL on `datafy.appmonitor.co.za` |

There is no full schema dump and no RLS in these files. Applying these three files to an empty database would not build the product. **Unknown** whether they have been applied to the live project.

Storage bucket constant: `media`. Placeholder object host also seen: `https://alb.datafyapp.co.za/storage/v1/object/public/uploads/R.png`.

`version` row used by CI: `id=1`, column `app_version`.

### PowerSync

URL: `https://67c1c2c9586c8d282a0669f2.powersync.journeyapps.com` (`FFAppConstants.PowerSyncUrl` and the header of `sync_rules.yaml`).

Local schema: JSON string `FFAppConstants.schema`. Tables include `survey`, `survey_multiple_choice`, `survey_questions`, `category_images`, `customer_user`, `app_user`, `store_user_link`, `kaching_gallery`, `survey_store_link`, `survey_responses`, `kaching_linked_users`, `store_promo`, `promo_gallery`, `promos_linked_users`, `product_images`, `product_store`, `task`, `attachments_queue` (`local_only: true`), `user_activity`.

`id` is omitted on purpose. PowerSync adds it. `user_activity` inserts use a UUID (README).

Services: `lib/services/powersync/power_sync_service.dart`, `power_sync_query.dart`, `power_sync_write.dart`, `powersync_actions.dart`.

Sync buckets in `sync_rules.yaml`:

| Bucket | Parameter | Data |
|---|---|---|
| `global` | none | all `survey`, `survey_multiple_choice`, `survey_questions`, `category_images` |
| `user_data` | `request.user_id()` as `id` | `customer_user` for that user; `app_user` where id matches and `LOWER(role) = 'rep'` |
| `user_stores` | `request.user_id()` as `user_id` | `store_user_link` for that user, `active = 1`, listed columns |
| `user_store_data` | each `store_code` linked to the user | products, images, open tasks, KaChing, promos, galleries, incomplete store surveys, survey responses for that store |
| `user_surveys` | `request.user_id()` | `survey_store_link` and `survey_responses` where `store_code` is null and the user matches |

`user_store_data` does not filter by the selected `customer_id`. README: deploy this file in the PowerSync dashboard after edits. Live dashboard contents were not readable from git.

`product_images` includes `processed_genai`, `genai_result`, `genai_message`, `genai_error`, and `project_version_arn`. No client screen in the route list was identified as the GenAI worker. **Unknown** which service writes those columns.

### Device permissions

Android manifest: `INTERNET`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, `CAMERA`, and read/write external storage up to SDK 32.

Login will not open Brands without location permission.

## Background work

No Dart isolate worker or queue consumer is the product's job runner.

- PowerSync sync and the attachment helper run inside the Rep client.
- `main.dart` swallows transient SQLite lock errors (`SqliteException(5)` / `database is locked`) when the stack is `sqlite_async`.
- Bitbucket Pipelines is the only scheduled-style automation in repo: version bump, Play upload, optional `version` PATCH.
- Tables named `store_processing_queue`, `product_store_processing_queue`, and `queue_processing_log` exist as generated classes. Their workers are not in this repo.

## Deployment

`bitbucket-pipelines.yml`:

| Trigger | Steps |
|---|---|
| Default (other branches) | `flutter pub get` |
| `main` | SonarQube pipe `sonarsource/sonarqube-scan:3.0.2` |
| `develop` | Android release AAB, then Play production |
| Custom `diagnostic-build` | Android build with extra logs, JDK 21 |
| Custom `deploy-production` | Same as `develop` |
| Custom `build-only` | Android AAB only |
| Custom `build-ios` | iOS build, no codesign step name on the build anchor; runner `self.hosted` + `macos` |
| Custom `deploy-ios-testflight` | iOS build then `fastlane beta` |

Android deploy image: `ruby:3.2`, `bundle exec fastlane production` from `android/`. Track `production`. Skips metadata, images, and screenshots.

Version bump (`scripts/bump_version.sh`): patch + 1, version code `100000 + BITBUCKET_BUILD_NUMBER`. Commits pubspecs and pushes with `[skip ci]` (`git push` failure is `|| true`).

Then PATCH `{SUPABASE_PROJECT_URL or SUPABASE_URL}/rest/v1/version?id=eq.1` with the service role. Failure is logged.

iOS (`ios/fastlane/Fastfile` lane `beta`): optional Match readonly, increment build, `build_app` scheme `Runner` export `app-store`, `upload_to_testflight` with `skip_submission: true`, `distribute_external: false`, `skip_waiting_for_build_processing: true`.

This clone's only remote branch is `origin/develop`. Sonar on `main` was not executed here. Whether `main` exists on the remote is **Unknown / Requires Confirmation** (single-branch clone).

Operator docs in repo, not duplicated here:

- `README.md`
- `BITBUCKET_MACOS_RUNNER_SETUP.md`
- `bitbucket_pipelines_flutter_cicd.md`
- `bitbucket_pipelines_flutter_cicd222.md`
- `flutter_playstore_deployment_guide.md`
- `POWERSYNC_OFFLINE_SYNC_RULES.md`
- `NewKnowledgeBase.md`

`README.md` also points at `../COMPREHENSIVE_SCOPE.md` and `../documentation/`, which are not in the git tree.

## Security notes (from the tree, not a pentest)

- A Supabase anon key default is in `lib/backend/supabase/supabase.dart`. CI can override it. The literal is still in git. Human review should decide on rotation and removal. The value is not repeated in Silicon Ark.
- The pipeline service role is a Bitbucket variable, not a file in git. README says not to commit keystores, `key.properties`, Play JSON, or `AuthKey_*.p8`.
- `requireAuth` is not enabled on routes. Admin routes check role only.
- Store isolation in the client is not a substitute for RLS. RLS is not in the committed migrations.
- Sync bucket `global` replicates all survey definitions and category images to every PowerSync client that connects.
- Force-update iOS URL uses a placeholder App Store id.
- `test/widget_test.dart` does not cover auth or isolation. It pumps `MyApp` and still comments a counter smoke test.
- Sonar project key: `overdrive_flutter_app_70dee6c4-fe61-42b4-b37c-c566c62a3aec` (`sonar-project.properties`). Scan step is on `main` only.

## Development and testing

README local check: `.\scripts\test-pipeline-local.ps1` (`flutter pub get` and `flutter build appbundle --release`).

`flutter_launcher_icons`: Android and web from `appicons/AppIcons/Assets.xcassets/AppIcon.appiconset/1024.png`. iOS icons are copied from `appicons/`, not regenerated (`ios: false` in the pubspec launcher config).

Assets declared: `assets/fonts/`, `images/`, `videos/`, `audios/`, `rive_animations/`, `pdfs/`, `jsons/`, `environment_values/`.

Dev dependency `sqlparser` is present. No migration runner or test that executes it was found.

`analysis_options.yaml` exists. Lint package `flutter_lints` 4.0.0.

## Monitoring

No analytics SDK, crash reporter, or APM dependency stood out in `pubspec.yaml`. Login and survey actions write `user_activity` (documented types include `login`, `survey_start`, `survey_submit`). PowerSync and force-update use `debugPrint` / `dart:developer` logs (`ForceUpdate:` prefix).

SonarQube is the only analysis integration in the pipeline, and only on `main`.

## Related hosts named in source

| Host | Use |
|---|---|
| `datafy.appmonitor.co.za` | Default Supabase URL. DISCHEM logo URL in the migration |
| `alb.datafyapp.co.za` | Public placeholder image |
| `67c1c2c9586c8d282a0669f2.powersync.journeyapps.com` | PowerSync |
| `play.google.com/store/apps/details?id=coza.datafy.web` | Force-update Android link |
| `apps.apple.com/app/id0000000000` | Force-update iOS link, placeholder |
| `storage.googleapis.com/flutterflow-io-6f20.appspot.com/.../R.png` | FlutterFlow placeholder, display only |

## Requires confirmation

- Live Supabase schema versus the generated Dart classes, and whether the three migrations ran.
- Live PowerSync rules versus `sync_rules.yaml`.
- Existence and role of git branch `main`.
- Service that fills store health counts and GenAI columns.
- Whether web is a supported release. The `web/` tree is present. The pipeline does not build it.
- Removal or rotation of the committed anon key default.
