DEV Community

AZAN Tech Atlas
AZAN Tech Atlas

Posted on Fully Autonomous

Building High-Throughput Micro-SaaS in 2026: The Modular Stack Architecture (FlutterFlow, Supabase & Celery)

Use FlutterFlow for the client, Supabase for authentication and PostgreSQL, and Celery for background execution. Put a server-side API between the client and privileged job creation.

The important boundary is the transaction:

Authenticated request
→ validate ownership, quota, and idempotency key
→ commit job + outbox event in PostgreSQL
→ dispatcher publishes event to the broker
→ Celery worker executes and persists the result
→ client reads authorized status
Enter fullscreen mode Exit fullscreen mode

This is an implementation blueprint, not a measured throughput benchmark. Capacity depends on workload, database connections, broker configuration, and worker concurrency. The migration below has not been executed as part of this article and must be validated in staging.

Disclosure: This article was prepared with AI assistance for AZAN Digital. We maintain AZAN App Nexus, a related SaaS architecture reference, and Azan Dev Hub, a developer utility reference for JSON formatting and campaign tracking workflows. They are supplementary resources, not dependencies of this design.

1. Define the trust boundaries

The client can authenticate, manage its own saved workflow definitions, request execution through the API, and read its own job status.

The backend controls validated job creation, quotas, outbox delivery, execution state, results, and privileged third-party credentials. Never place Supabase service-role credentials, broker credentials, or provider secrets inside a FlutterFlow application.

2. Apply a PostgreSQL migration with explicit ownership rules

This migration targets a fresh Supabase project. Backend-owned tables intentionally have no client write grants. Run it through an administrative deployment connection; provision narrowly scoped API, dispatcher, and worker roles separately. Do not reuse the migration owner for normal application execution.

begin;

create table public.workflow_definitions (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null default auth.uid()
    references auth.users(id),
  name text not null
    check (char_length(name) between 1 and 200),
  definition jsonb not null default '{}'::jsonb
    check (jsonb_typeof(definition) = 'object'),
  created_at timestamptz not null default now()
);

create index workflow_definitions_owner_created_idx
  on public.workflow_definitions (owner_id, created_at desc);

alter table public.workflow_definitions enable row level security;

revoke all on public.workflow_definitions
  from public, anon, authenticated;
grant select, delete
  on public.workflow_definitions to authenticated;
grant insert (name, definition)
  on public.workflow_definitions to authenticated;
grant update (name, definition)
  on public.workflow_definitions to authenticated;

create policy workflows_select
  on public.workflow_definitions
  for select to authenticated
  using ((select auth.uid()) = owner_id);

create policy workflows_insert
  on public.workflow_definitions
  for insert to authenticated
  with check ((select auth.uid()) = owner_id);

create policy workflows_update
  on public.workflow_definitions
  for update to authenticated
  using ((select auth.uid()) = owner_id)
  with check ((select auth.uid()) = owner_id);

create policy workflows_delete
  on public.workflow_definitions
  for delete to authenticated
  using ((select auth.uid()) = owner_id);

create table public.execution_jobs (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null references auth.users(id),
  idempotency_key text not null
    check (char_length(idempotency_key) between 1 and 200),
  request_hash text not null
    check (request_hash ~ '^[0-9a-f]{64}$'),
  input_snapshot jsonb not null
    check (jsonb_typeof(input_snapshot) = 'object'),
  status text not null default 'queued'
    check (status in ('queued', 'running', 'succeeded', 'failed')),
  attempts integer not null default 0 check (attempts >= 0),
  lease_until timestamptz,
  result jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (owner_id, idempotency_key)
);

create index execution_jobs_owner_created_idx
  on public.execution_jobs (owner_id, created_at desc);

create index execution_jobs_recovery_idx
  on public.execution_jobs (lease_until)
  where status = 'running';

alter table public.execution_jobs enable row level security;

revoke all on public.execution_jobs
  from public, anon, authenticated;
grant select on public.execution_jobs to authenticated;

create policy jobs_select
  on public.execution_jobs
  for select to authenticated
  using ((select auth.uid()) = owner_id);

create schema app_private;
revoke all on schema app_private from public, anon, authenticated;

create table app_private.outbox (
  id uuid primary key default gen_random_uuid(),
  job_id uuid not null unique
    references public.execution_jobs(id),
  created_at timestamptz not null default now(),
  available_at timestamptz not null default now(),
  lease_until timestamptz,
  delivery_attempts integer not null default 0
    check (delivery_attempts >= 0),
  published_at timestamptz
);

revoke all on app_private.outbox from public, anon, authenticated;

create index outbox_pending_idx
  on app_private.outbox (available_at, created_at)
  where published_at is null;

commit;
Enter fullscreen mode Exit fullscreen mode

Owner indexes support owner-scoped queries. Column-level grants prevent clients from changing ownership, identifiers, or creation timestamps. WITH CHECK validates ownership of inserted or updated rows. For client inserts and updates, send only the granted fields: name and definition.

The job status and result remain backend-owned. The backend must maintain updated_at, leases, and state transitions explicitly; the schema does not implement those handlers. Review result payloads before making them client-readable.

This example uses individual ownership. Organization tenancy requires membership-based policies and tests for every role. Foreign keys also require a deliberate account-deletion and data-retention workflow.

3. Make job creation idempotent

Require an Idempotency-Key on execution requests. The server should:

  1. Verify the access token and derive the owner from it.
  2. Validate the workflow and execution quota.
  3. Canonicalize the validated request and compute its SHA-256 hash.
  4. Open a database transaction.
  5. Insert the job and its outbox record.
  6. Commit, then return 202 Accepted with the job ID.

The uniqueness constraint scopes the key to the owner. If the same owner repeats a key with the same request hash, return the original job. If the hash differs, return 409 Conflict.

Store an immutable input snapshot. A retry must not execute a workflow that changed after submission. Include execution-relevant options and workflow version in the hashed request.

After an insert conflict, fetch the existing row in a subsequent statement and compare hashes. Handle concurrent requests explicitly. Enforce quota reservations in the transaction rather than using a race-prone read-then-write check.

4. Use a transactional outbox

Calling task.delay() after committing a job leaves a failure window: the process can crash after saving the job but before publishing its message. Commit the outbox event in the same transaction as the job.

A dispatcher should:

  1. Claim a bounded batch of eligible events using row locks and SKIP LOCKED.
  2. Set a delivery lease and commit the claim.
  3. Publish job IDs with the broker's supported durable-delivery configuration.
  4. Mark events published only after the appropriate broker confirmation.
  5. Retry expired leases and failed publications with backoff.

Keep network calls outside long database transactions. Use a claim token or equivalent fencing condition so an expired dispatcher cannot overwrite a newer claim.

A crash after publication but before saving published_at can produce duplicate messages. The worker must tolerate them. The outbox provides recoverable publication, not exactly-once execution.

5. Design Celery tasks for retries

Send identifiers through the broker rather than full customer payloads. At task start, atomically claim the job, return if it already succeeded, and avoid taking over a valid execution lease. Increment the attempt count and assign an execution lease or fencing token before loading the immutable snapshot.

Retry transient failures such as rate limits and temporary network errors. Use bounded exponential backoff with jitter. Permanent authorization errors and invalid inputs should reach a terminal state.

Late acknowledgements can help with some failure cases, but they do not make side effects idempotent or guarantee recovery from every worker termination. Review Celery's documented worker-loss behavior before configuring redelivery.

Protect external effects separately:

  • Use downstream idempotency mechanisms when available.
  • Store external operation identifiers.
  • Enforce uniqueness for locally recorded effects.
  • Reconcile ambiguous timeouts before repeating irreversible operations.

Run a recovery process for expired execution leases and stuck jobs. Long-running jobs need lease renewal; reject stale worker writes using the current lease token.

A database transaction cannot atomically include an arbitrary external API call. Model that boundary explicitly.

6. Keep workflow JSON portable and reviewable

Treat imported workflow JSON as configuration, not trusted executable code. When consulting a reference library such as App Nexus, verify each blueprint's schema, supported versions, required secrets, and deployment assumptions before importing it.

For every workflow:

  • Assign a schema version.
  • Validate allowed node and action types.
  • Keep credentials as secret references.
  • Restrict outbound destinations where appropriate.
  • Set execution, payload-size, and retry limits.
  • Store the exact validated version with each job.

For manual inspection, Azan Dev Hub is a related reference for JSON formatting and campaign-tracking workflows. Use sanitized examples; keep customer payloads, tokens, and signed URLs out of public utilities.

7. Pre-launch reliability checklist

Database and authorization

  • [ ] Apply every migration to an empty database.
  • [ ] Test upgrading from the previous deployed schema.
  • [ ] Test policies using ordinary authenticated sessions.
  • [ ] Verify user A cannot read, update, or delete user B's records.
  • [ ] Verify clients cannot change ownership or execution status.
  • [ ] Verify anonymous access is denied.
  • [ ] Test membership changes if organization tenancy is added.

Async execution

  • [ ] Repeat an identical key and receive the original job.
  • [ ] Reuse the key with different input and receive 409.
  • [ ] Crash the API after commit but before publication.
  • [ ] Crash the dispatcher after publication but before bookkeeping.
  • [ ] Deliver the same Celery message twice.
  • [ ] Terminate a worker and verify lease recovery.
  • [ ] Test stale-worker result writes.
  • [ ] Exhaust retries and verify an inspectable terminal failure.

Recovery and operations

  • [ ] Restore database and uploaded objects into an isolated environment.
  • [ ] Measure recovery time and recoverable data loss.
  • [ ] Alert on outbox age, queue delay, expired leases, and error rates.
  • [ ] Load-test realistic payloads and worker concurrency.
  • [ ] Limit database connections to the available connection budget.
  • [ ] Verify secrets are absent from generated clients, logs, and exports.

Deploy when these failure paths have been demonstrated, not merely when the happy-path job completes.

Implementation references

Top comments (0)