⚡ Optimistic CRUD Pipeline: The Factorio Pattern
A modern, event-driven approach to CRUD operations that delivers instant UX while keeping the backend resilient and maintainable.
Why This Pattern?
Deleting or renaming a scan currently blocks the UI for 4-5 seconds while we synchronously:
- Remove the image from Supabase Storage
- Delete
job_queuerows - Delete the
scan_uploadsrow
That delay is network bound and won’t disappear in production. We need a design that provides perceived instant feedback while doing heavy work in the background – just like erasing a tweet on Twitter.
Core Principles
- Optimistic UI – mutate local state immediately, rollback on error.
- Soft Deletes – never hard-delete in the request path; set
deleted_at. - CQRS – write operations are commands; reads come from pre-filtered views.
- Event-Driven – commands raise domain events handled by background workers.
- Idempotency – every handler safe to run twice.
- Observability – structured logs, metrics, DLQ for failed jobs.
High-Level Flow
- User clicks Delete Scan.
- Frontend dispatches
optimisticDelete(scanId)→ item disappears. - API
POST /api/commands/delete-scanwrites a row tocommand_queue. - API returns
202 AcceptedwithcommandIdin <100 ms. - Worker (Node or Python) picks up
DeleteScanCommand, executes: - Remove file from Storage
- Delete related
job_queuerows - Hard-delete
scan_uploadsrow - On success an
ScanDeletedEventis emitted (for metrics, cache purge).
Database Changes
- Add
deleted_at TIMESTAMPTZ&version INT DEFAULT 0toscan_uploads. - Create
command_queuetable:id UUID PK, type TEXT, payload JSONB, created_at TIMESTAMPTZ DEFAULT now(), processed_at TIMESTAMPTZ
API Layer
We expose /api/commands/* endpoints that only insert into command_queue. They run on Vercel Edge or Next.js API, complete in under 100 ms, and never block on Supabase Storage.
Background Processing
- Preferred: Supabase Edge Function (TypeScript) subscribed to
command_queue. - Fallback: Existing Python worker polls every few seconds.
- Each command handled in isolation and retried on failure.
Frontend Integration
- React Query mutation with
onMutate→ optimistic removal. - Use
onErrorto rollback if command rejected. - Poll or subscribe to
scansview (excluding soft-deleted rows) for eventual consistency.
Implementation Roadmap
- D1: Add
deleted_atcolumn & createcommand_queue. - D1: Refactor delete/rename API to enqueue commands.
- D2: Build minimal Edge Function handler for
DeleteScanCommand. - D2: Update React Query mutations to optimistic UI.
- D3: Extend pattern to Rename, Retry, and other heavy ops.
Reference Snippet
// deleteScan.ts (client)
await deleteScanMutation.mutateAsync(scanId);
// /api/commands/delete-scan (server)
export async function POST(req: NextRequest) {
const { scanId } = await req.json();
await supabase.from('command_queue').insert({
type: 'DELETE_SCAN',
payload: { scanId }
});
return NextResponse.json({ accepted: true }, { status: 202 });
}
// edge_function/delete_scan.ts
if (command.type === 'DELETE_SCAN') {
const { scanId } = command.payload;
// 1) Delete storage
// 2) Purge job_queue rows
// 3) Hard-delete scan_uploads
// 4) Mark command processed
}