Learn from OSS
Midday

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-renders

Workflow 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

  1. Exchange token — API calls the provider to convert public token → access token
  2. Fetch accounts — API calls provider to list available accounts (checking, savings, etc.)
  3. Store in DBbank_connections record (provider, access token encrypted) + bank_accounts records (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 balance

Transactions 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:

  1. Generate PDF@midday/invoice uses @react-pdf/renderer to produce a PDF
  2. Send email@midday/notifications sends via Resend with a React Email template (invoice.tsx)
  3. Update status — Invoice status changes to sent
  4. 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 automatically

Why 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 Storage

Document 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 record

AI 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 dashboard

User 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 model

Model 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:

  1. Sum hours per entry
  2. Apply project hourly rate
  3. Create line items from entries
  4. 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

FromToMechanismExample
Browser → APItRPC over HTTPSCreating invoices, querying transactions
Browser → APIREST over HTTPSDesktop app, third-party integrations
API → PostgreSQLDrizzle ORMAll data operations
API → RedisCache set/getAPI key lookup, user sessions, rate limiting
API → BullMQ (via Redis)Job queueBank sync, document processing
Plaid/GoCardless → APIREST API (OAuth)Fetching bank transactions
Stripe → APIWebhook (POST)Payment confirmations
Gmail/Outlook → WorkerREST API (OAuth)Email receipt sync
Worker → AzureREST APIOCR / document intelligence
AI SDK → OpenAIREST APIFinancial assistant queries
API → ResendREST APISending invoice and notification emails
Trigger.dev → WorkerScheduled triggersRecurring invoices, bank sync polling

What's Next