ProductionProductSystems

RODIFT

Enterprise field issue-reporting platform for a field-sales organization. Staff report outlet problems from a mobile app; each issue is geo-matched to the nearest outlet, routed through the org role hierarchy, and driven to resolution with push notifications and an auditable trail. A web dashboard gives management oversight.

FlutterDartNext.jsSupabasePostgresPostGISSQL
Private repositoryPrivacy policy
Tagged release v2.0.0Published privacy policy

The problem

Field staff had no reliable way to report outlet problems, and management had no view of what was outstanding.

Context and constraints

A distribution and field-sales organisation runs thousands of retail outlets across a multi-level territory hierarchy. When something was wrong at an outlet there was no structured way to capture it with photo and GPS proof from the field, work out which outlet and which responsible people it belonged to, and drive it to resolution with an audit trail.

  • Field staff report from phones in the field, so location has to come from the device and be trusted only after the server re-checks it.
  • The organisation hierarchy is the routing logic: an issue has to reach the right people through the chain of command with no manual triage.
  • Two audiences with different needs — an operational mobile app for field roles, and a management dashboard on the web. The field-only role is deliberately blocked from the web entirely.
  • The client reduced the web scope mid-project; user, outlet and area administration modules were removed from the dashboard.
  • No email or SMS channel was available for account recovery, which forced an in-app approval design rather than a reset link.

What I built

A Flutter mobile app for the seven operational roles, a Next.js management dashboard, and a Supabase backend that holds the business logic. Reporting an issue captures a photo and GPS position, matches it to the nearest outlet, resolves the responsible people from the organisation hierarchy, and drives the issue to resolution with push and in-app notifications over an immutable audit trail.

Architecture

Two clients over one Supabase backend. Business logic lives in Deno edge functions and SQL rather than in either client, so the mobile app and the dashboard cannot disagree about who owns an issue.

reportmanageremindersphotoauthorizeassignnotifyFlutter appField reporting, 7 rolesNext.js dashboardManagement oversightpg_cronReminder schedulerEdge functionsDeno, business logicAuth + RLSRoles and row scopingPostgreSQL + PostGISGeo and hierarchyObject storageIssue photosPush deliveryFCM HTTP v1
Sanitized architecture of a private client system, drawn from the project documentation. It shows implemented components and their relationships; it is not a screenshot, and no client data, identifiers or configuration are shown.
Flutter app
Field reporting, 7 roles
Next.js dashboard
Management oversight
pg_cron
Reminder scheduler
Edge functions
Deno, business logic
Auth + RLS
Roles and row scoping
PostgreSQL + PostGIS
Geo and hierarchy
Object storage
Issue photos
Push delivery
FCM HTTP v1

My contribution

Sole engineer — mobile app, web dashboard, backend and database design.

Technical decisions

What was chosen, why, and what it cost.

Encode the organisation hierarchy in the data model

Decision
The geography (area, territory, distribution, outlet) and the chain of command are modelled as first-class data rather than handled by assignment rules in application code.
Why
Reporting an issue can then resolve the responsible people automatically, so no one triages a queue by hand and the routing cannot drift between the two clients.
Trade-off
The model is rigid: an organisational restructure is a data migration, not a configuration change.

Re-validate the client-chosen outlet on the server

Decision
When the app confirms which outlet an issue belongs to, the create-issue function independently re-checks that the outlet exists, is active and is within range before trusting it, and the accuracy value reported by the device cannot widen the acceptance radius beyond a server-side cap.
Why
GPS position and accuracy come from a device in someone else's hands. Treating either as authoritative would let a report be attached to the wrong outlet.
Trade-off
A genuine report from a position with poor accuracy can be rejected and has to be retried.

Three layers of authorization rather than one

Decision
Access is enforced at row-level security, in SQL functions, and again in the edge functions, with role hierarchy and territory scoping applied at each layer.
Why
Two clients and a scheduler all reach the same data. A single enforcement point would mean any one of them bypassing it becomes a full authorization bypass.
Trade-off
A permission change usually has to be made in more than one place, and the layers have to be kept consistent.

Human-approved account recovery instead of a reset link

Decision
Password resets are approved by a person in the hierarchy and completed in-app on the requesting device using a one-time hashed token, with a single pending request, a cooldown and attempt caps.
Why
There was no email or SMS channel available, so the usual reset-link flow was not an option.
Trade-off
Recovery depends on an approver being available, and reinstalling the app or changing device means starting a new request.

Make the recovery endpoint unable to confirm an account exists

Decision
An unknown login, an ineligible account, an active cooldown and an already-pending request all return a response identical in shape and status to a genuine new request.
Why
A recovery endpoint that answers differently for real and fake accounts is an account-enumeration oracle, and this one is reachable without logging in.
Trade-off
A user who mistypes their login gets an apparently successful response and has to wait for an approval that will never arrive.

Security and reliability

The failure modes that shaped the implementation.

Privileged keys stay server-side

The service-role key is confined to server-side API routes, edge functions and the scheduler vault. It is never shipped in the browser bundle or the mobile app.

Bulk enumeration of the outlet master

Nearby-outlet lookups are scoped to the caller's own territory or area for field roles, so the endpoint cannot be walked to extract the full outlet list.

Immutable audit trail

Audit logs and the issue timeline are append-only, so the resolution history of an issue cannot be rewritten after the fact.

Issue photos are URL-addressable

Photos live in a public bucket and are reachable by anyone holding the URL. This is a deliberate trade for delivery simplicity and is recorded as such rather than presented as access control.

Verification

How the implementation was checked, and how much of that can be shown publicly.

  • Pure-logic assignment and escalation proofsevidenced

    Two standalone suites check outlet-based assignment and the reset-approver hierarchy and escalation without touching a database — 7 and 15 checks respectively.

  • Integration suite with a production-safe defaultevidenced

    A pytest suite runs against a Supabase project. Data-mutating tests are marked and skipped unless explicitly enabled, so the default run cannot write to production.

  • Static checks across all three tiersevidenced

    TypeScript compilation for the web app, deno check for edge functions, and flutter analyze for the mobile app.

  • Tagged releaseevidenced

    A v2.0.0 release tag exists in the private repository, and a public privacy policy is published as required for mobile store distribution.

Results

  • Reporting an issue resolves the responsible people automatically from the hierarchy, replacing manual triage.
  • A route-matching defect that caused all four routes of the reporting function to return 404 was found and fixed, and the fix was verified against a running environment.
  • Report aggregation was paginating at a fixed 200 rows, which silently under-counted once an organisation passed that many matching issues; it now pages through the full result set.

Timeline

  1. v2.0.0 tagged

    Release tag in the private repository.

  2. Product rebrand

    The product was renamed in July 2026. Backend identifiers already baked into production accounts and devices were deliberately left unchanged, because renaming them would have broken existing logins or forced a reinstall on every device.

  3. Privacy policy published

    A prerequisite for mobile store distribution.

Limitations and disclosure

What this project does not do, and what cannot be shown publicly.

  • Client-owned system; the repository and operational data cannot be made public.

This is client-owned work. The repository, the client, the operational data and the deployment configuration are not public, and nothing on this page reproduces them. What is shown is the architecture, the engineering decisions and the verification approach, described from the project documentation. No usage figures, user counts or business outcomes are claimed, because none are available to publish.

Other projects in the same engineering domains.