Workflows & Data Flows
Key user journeys in Midday — bank sync, invoicing, receipt matching, AI assistant, and time tracking
Workflows & Data Flows
This page traces the most important user journeys through Midday — from user actions in the browser through the API, database, and background workers. Understanding these flows reveals how a modern fintech SaaS handles real-world business logic.
Reading Guide
Each workflow shows the complete data path from user action to final result. Pay attention to where external services (Plaid, Stripe, OpenAI) enter the picture — these integration points are where fintech apps get complex.
How a Request Flows Through Midday
Browser (User Action)
│
▼
React Component ← dashboard UI
│ calls tRPC mutation/query (or server action)
▼
Next.js Server / Hono API ← validates JWT, runs business logic
│
├──► Drizzle ORM (PostgreSQL) ← data operations
├──► External APIs ← Plaid, Stripe, OpenAI
└──► BullMQ / Trigger.dev ← queue background jobs
│
▼
Response → React Component ← TanStack Query caches, UI re-rendersWorkflow 1: Connecting a Bank Account
The foundational flow — linking a bank so transactions auto-import.
User clicks "Connect Bank"
The dashboard opens Plaid Link (US/CA), GoCardless Connect (EU), or Teller Connect depending on the user's region.
User authenticates with their bank
The banking provider's widget handles the bank login (multi-factor auth, account selection). Midday never sees the user's bank credentials.
Provider returns a connection token
The banking widget returns a public token/requisition ID. The dashboard sends it to the API:
POST /trpc/bankAccounts.create
Body: { provider: "plaid", publicToken: "..." }API exchanges token and stores connection
- Exchange token — API calls the provider to convert public token → access token
- Fetch accounts — API calls provider to list available accounts (checking, savings, etc.)
- Store in DB —
bank_connectionsrecord (provider, access token encrypted) +bank_accountsrecords (name, balance, currency, type)
Initial transaction sync triggers
A BullMQ job is queued to fetch historical transactions:
Worker: TransactionSyncJob
1. Call provider API: getTransactions(last 90 days)
2. For each transaction:
a. Store in `transactions` table
b. Queue AI categorization (match against category embeddings)
c. Queue enrichment (merchant name, logo via provider data)
3. Update bank_account balanceTransactions appear in the dashboard
TanStack Query invalidates the transaction list cache. The user sees their bank transactions, auto-categorized and enriched.
For PMs: Why Is Bank Sync Complex?
Each banking provider has different: (1) auth flows (OAuth, mTLS certificates, JWT), (2) data formats (Plaid uses internal IDs, GoCardless uses ISO 20022), (3) rate limits, (4) sync patterns (webhook vs polling), and (5) error handling (expired tokens, revoked access). The @midday/banking abstraction normalizes all of this into a single interface, but the underlying complexity is substantial.
Workflow 2: Creating and Sending an Invoice
User creates an invoice
The invoice editor lets users add line items (invoice_products), set payment terms, choose a template, and select a customer.
Invoice is saved
tRPC: invoices.create → Drizzle INSERT into `invoices` + `invoice_products`The invoice number is auto-generated (configurable format). The status is set to draft.
User clicks "Send"
The dashboard calls the API to send the invoice:
- Generate PDF —
@midday/invoiceuses@react-pdf/rendererto produce a PDF - Send email —
@midday/notificationssends via Resend with a React Email template (invoice.tsx) - Update status — Invoice status changes to
sent - Create Stripe payment link — If Stripe Connect is configured, a payment intent is created
Customer receives and pays
The customer gets an email with a link to the invoice. The link opens a public page where they can:
- View the invoice
- Pay via Stripe (credit card, bank transfer)
- Download the PDF
Stripe webhook confirms payment
Stripe sends POST /webhooks/stripe
Event: payment_intent.succeeded
│
▼
Update invoice status → "paid"
Update invoice.paid_at timestamp
Record payment details
│
▼
Notify user via email (invoice-paid.tsx template)Recurring invoices (optional)
If the invoice is set to recurring, Trigger.dev schedules the next send:
Trigger.dev: InvoiceSchedulerTask
Runs on schedule → creates new invoice from template → sends automaticallyWhy Stripe Connect?
Midday uses Stripe Connect (not regular Stripe) for invoice payments. This means the money flows directly to the freelancer's Stripe account — Midday acts as a platform facilitating the payment, not as a middleman holding funds. This is legally important for a tool used by independent businesses.
Workflow 3: Magic Inbox (AI Receipt Matching)
The inbox automatically matches incoming receipts to bank transactions.
Receipts arrive via email
The @midday/inbox package syncs from connected Gmail or Outlook accounts:
Worker: InboxSyncJob
1. Fetch new emails from Gmail API / Microsoft Graph
2. Filter for receipt-like attachments (PDF, images)
3. Store in `inbox` table with file in Supabase StorageDocument Intelligence extracts data
Worker: DocumentProcessingJob
1. Send PDF/image to Azure Document Intelligence (OCR)
2. Extract: merchant name, amount, date, currency
3. Store extracted data on inbox recordAI matches receipt to transaction
Worker: InboxMatchingJob
1. Query `transactions` for similar amount + date range + merchant
2. Use embedding similarity for fuzzy matching
3. If confidence > threshold:
→ Auto-link receipt to transaction (transaction_attachments)
4. If uncertain:
→ Store as suggestion (transaction_match_suggestions)
→ User reviews in dashboardUser reviews matches
Unmatched or low-confidence receipts appear in the Inbox view. The user can manually link them to transactions or dismiss them.
Workflow 4: AI Financial Assistant
The chat assistant answers natural language questions about your finances.
User types a question
For example: "What's my burn rate this quarter?"
The dashboard sends the message to the AI chat endpoint.
AI SDK selects and executes tools
AI SDK (OpenAI / Anthropic / Google):
1. Parse user intent
2. Select tool: "get-burn-rate"
3. Tool executes: Drizzle query against transactions
→ SUM(amount) WHERE date IN quarter, GROUP BY month
4. Return structured data to the modelModel generates a response
The AI model receives the financial data and generates a natural-language response with the burn rate breakdown. The response is streamed to the user via AI SDK React hooks for instant feedback.
Suggested follow-ups
The assistant suggests related questions: "Compare to last quarter?" or "What are my top 3 expenses?"
For PMs: AI Tools vs Free-Text
The assistant doesn't guess answers from training data. It uses structured tools that execute real database queries. When you ask about burn rate, the AI calls a get-burn-rate function that runs an actual SQL query against your transaction data. The AI's role is understanding the question and formatting the answer — the data is always real and current.
Workflow 5: Time Tracking → Invoice
How tracked time becomes a billable invoice.
User tracks time on a project
The time tracker records entries against a tracker_project:
- Start/stop timer (live tracking)
- Manual entry (date, duration, description)
- Entries stored in
tracker_entries
User generates an invoice from tracked time
The dashboard aggregates billable entries for a project and pre-fills an invoice:
- Sum hours per entry
- Apply project hourly rate
- Create line items from entries
- Link invoice to customer and project
Invoice follows the standard send/pay flow
Same as Workflow 2 — generate PDF, send via email, collect payment via Stripe.
Information Flow Summary
| From | To | Mechanism | Example |
|---|---|---|---|
| Browser → API | tRPC over HTTPS | Creating invoices, querying transactions | |
| Browser → API | REST over HTTPS | Desktop app, third-party integrations | |
| API → PostgreSQL | Drizzle ORM | All data operations | |
| API → Redis | Cache set/get | API key lookup, user sessions, rate limiting | |
| API → BullMQ (via Redis) | Job queue | Bank sync, document processing | |
| Plaid/GoCardless → API | REST API (OAuth) | Fetching bank transactions | |
| Stripe → API | Webhook (POST) | Payment confirmations | |
| Gmail/Outlook → Worker | REST API (OAuth) | Email receipt sync | |
| Worker → Azure | REST API | OCR / document intelligence | |
| AI SDK → OpenAI | REST API | Financial assistant queries | |
| API → Resend | REST API | Sending invoice and notification emails | |
| Trigger.dev → Worker | Scheduled triggers | Recurring invoices, bank sync polling |