> SUPERSEDED 2026-05-26 — Single-tenant BMW conversion (refactor/remove-control-plane). Historical record only.

# ADR 005: Provisioning Execution Model — after() as Interim Bridge

**Status**: Accepted (V1 interim) — supersede with ADR-006 when durable queue is adopted  
**Date**: 2026-04-07  
**Deciders**: Platform engineering

---

## Context

Tenant provisioning involves 8+ sequential steps (create Supabase project, apply migrations, seed branding, etc.) that can take 2–5 minutes total. The `/provision` API route must return quickly to the caller while execution continues in the background.

We need a background execution mechanism that:
1. Returns an HTTP response immediately (202 Accepted)
2. Allows UI to poll for progress
3. Is safe to resume if a step fails partway through

---

## Decision

**V1**: Use Next.js `after()` from `next/server` to run `runProvisioningSteps(jobId)` after the HTTP response is sent, within the same Vercel function instance. Set `export const maxDuration = 300` on the provision route to extend the function lifetime to 5 minutes.

The execution mechanism is encapsulated behind a `ProvisioningDispatcher` interface so that switching to a durable queue requires only changing the factory function in `lib/provisioning/dispatcher.ts`.

**V2 target**: Replace `AfterDispatcher` with `QueueDispatcher` once a durable queue (Vercel Queues, Inngest, or BullMQ) is provisioned. The step execution code (`runProvisioningSteps`) is unchanged.

---

## Rationale

### Why `after()` for V1

- Zero infrastructure dependencies — works in any Vercel deployment today
- Steps are already idempotent and resumable; if the job is abandoned, manual retry recovers safely
- Provisioning is a low-frequency, operator-triggered action (not user-facing at scale)
- Time-to-working-system is lower than introducing a queue now

### Why not inline (synchronous) execution

Provisioning can exceed Vercel's default 10s response timeout and would block the HTTP client during the entire run. Unacceptable UX.

### Why not a queue today

No durable queue is currently provisioned. Adding one (Vercel Queues, Inngest, Redis) adds infrastructure complexity and cost that is not justified at pre-launch scale. The `ProvisioningDispatcher` abstraction means this is a one-file change when we do add a queue.

---

## Consequences

### Accepted limitations (V1)

- **Not crash-safe.** If the Vercel instance is restarted, replaced by a new deployment, or times out mid-run, the `after()` callback is silently abandoned. The job stays in `RUNNING` state indefinitely.
- **`maxDuration` dependency.** The provision route must keep `export const maxDuration = 300` as long as `AfterDispatcher` is in use. Removing it would cap execution at the default timeout and risk partial runs.
- **Manual recovery required** for stuck jobs. The workspace health check flags jobs stuck in `RUNNING` > 10 minutes. Operators use the retry endpoint to recover.

### Mitigations in place

- Job and step records are created synchronously before `after()` fires, so a crash always leaves a recoverable DB state.
- `runProvisioningSteps` skips `COMPLETED`/`SKIPPED` steps — retries are always safe.
- Dispatch errors are caught and logged; dispatch failure leaves the job `PENDING` (not abandoned silently).
- The idempotency key guard prevents double-triggering the same attempt.

### When to supersede this ADR

- Provisioning volume increases to the point where operator-assisted recovery is a burden
- Step execution regularly approaches 5 minutes (hitting `maxDuration`)
- SLA requirements demand guaranteed delivery without manual intervention

At that point, implement `QueueDispatcher` and create ADR-006.

---

## Alternatives considered

| Option | Decision |
|--------|----------|
| Inline synchronous execution | Rejected — blocks caller, hits response timeout |
| Vercel Queues (beta) | Deferred — adds infrastructure; `ProvisioningDispatcher` makes this a future one-line swap |
| Inngest durable functions | Deferred — same rationale as Vercel Queues |
| BullMQ + Redis | Deferred — requires Redis provisioning; overkill at current scale |
| Vercel Background Functions | Not available in current plan tier |
