Skip to content

Architecture Review

Section 1: Executive Summary

SeedTrust is a fintech platform that manages the financial layer of surrogacy and egg donation journeys. It holds escrow funds on behalf of Intended Parents, coordinates disbursement approvals between case managers and agencies, and processes payments to surrogates via ACH bank transfers, Huntington managed accounts, and credit card funding.

The system is in active architectural transition: a large Flask monolith (the original production system) runs alongside a FastAPI backend that powers a Next.js mobile PWA. The previous third-generation Go/SvelteKit rewrite plan has been superseded. The strategic direction is to consolidate product ownership into seedtrust_flask so SeedTrust can become one simpler codebase with fewer frameworks, fewer auth paths, and a Flask-centered UI using Jinja, Alpine, HTMX, and JSON endpoints for partial data updates. All active services share a single MySQL database today, which is both the source of truth and the tightest coupling point in the system.

graph TD
subgraph Clients
Browser["Desktop Browser\n(Flask-rendered HTML)"]
PWA["Mobile PWA\n(Next.js 15)"]
end
subgraph Backend
Flask["seedtrust_flask\nFlask 2.2 / Python 3.8\nLegacy Monolith\n:5001"]
FastAPI["seedtrustapi\nFastAPI / Python 3.12\nModern REST API\n:8000"]
end
subgraph Shared
Schema["seedtrust_schema\nShared SQLAlchemy\nColumn Definitions"]
DB[(MySQL\nShared Database)]
end
subgraph External
Huntington["Huntington Bank\nManaged Escrow Accounts"]
NACHA["NACHA / SFTP\nACH File Delivery"]
SendGrid["SendGrid\nEmail"]
Twilio["Twilio\nSMS"]
S3["AWS S3 / CloudFront\nDocument Storage"]
Stripe["Stripe / Alacriti\nCard Funding"]
PostHog["PostHog\nAnalytics & Error Tracking"]
end
Browser -->|Server-rendered pages| Flask
PWA -->|JWT REST calls| FastAPI
Flask -->|SQLAlchemy ORM| DB
FastAPI -->|Async SQLAlchemy| DB
Flask --> Schema
FastAPI --> Schema
Flask --> SendGrid
Flask --> S3
Flask --> NACHA
Flask --> Huntington
FastAPI --> SendGrid
FastAPI --> S3
FastAPI --> Huntington
FastAPI --> Twilio
FastAPI --> Stripe
FastAPI --> Alacriti
PWA --> PostHog
Flask --> PostHog

Section 2: Module Breakdown

Module 1 — Case Management

Purpose & Business Value

A case is the central organizing unit of the entire platform. It links an Intended Parent (the payer), a Surrogate or Donor (the recipient), and an Agency (the coordinator). Everything financial — escrow balances, payment schedules, disbursement requests — belongs to a case. Without cases, there is nothing to pay.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/case.py
Flask routes (Admin/CM)seedtrust_flask/seedtrust/views/cm.py (~57KB)
Flask routes (Agency)seedtrust_flask/seedtrust/views/owner.py (~76KB)
FastAPI serviceseedtrustapi/src/seedtrust/modules/case/service.py
FastAPI routeseedtrustapi/src/seedtrust/modules/case/route.py
Next.js pageapp/src/app/(authenticated)/case/[caseId]/page.tsx

Main Flows

  1. Admin creates a case → links IP, surrogate, agency → sets case type (surrogacy vs. egg donation) and contract stage
  2. Case moves through stages: inquiry → pre-GSA → post-GSA → closed
  3. GSA (Gestational Surrogacy Agreement) signing marks the legal threshold — before it, payment rules are more restricted
  4. Pregnancy information (due date, baby count, delivery date) is tracked and triggers milestone payments

Interactions With Other Modules

  • Owns all Disbursement Requests (DRs)
  • Owns the Compensation/SPC payment schedule
  • Owns the Ledger (transaction history)
  • Linked to ACH forms (banking details for all parties)
  • Linked to Escrow Agreement documents

Legacy Issues & Anti-patterns

  • cm.py and owner.py are each 50–76KB Flask view files with business logic, database queries, and HTML rendering all mixed together — no service layer
  • Case stage transitions are implicit (no formal state machine), making it easy to create cases in invalid states
  • app_flask.py is a 400KB+ file that registers all routes and contains ad-hoc logic — it is the primary “gotcha” file

Gotchas for New Developers

  • The ip_case table is the database table name; the Python class is Case. These names differ everywhere.
  • “Pre-GSA” and “Post-GSA” behavior differs significantly: some payment types, approval rules, and UI flows are gated on this stage. Always check which stage a case is in before assuming any payment behavior.
  • Case type affects which fields are relevant: a surrogacy case has pregnancy tracking; an egg donation case does not.

Module 2 — Disbursement Requests (DRs)

Purpose & Business Value

A Disbursement Request is how money moves out of escrow. A surrogate, IP, or case manager initiates a request for payment. It must travel through an approval chain before funds are released. This is the most heavily workflow-driven part of the system and the source of the most operational edge cases.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/disbursement_request.py
FastAPI moduleseedtrustapi/src/seedtrust/modules/case/disbursement_requests/
Auto-approverseedtrustapi/src/seedtrust/modules/auto_approver/
Next.js componentapp/src/app/(authenticated)/case/[caseId]/components/DisbursementRequests.tsx
Next.js add flowapp/src/app/(authenticated)/case/[caseId]/disbursements/add/

Main Flows

Submit DR → CM Review (pre-GSA cases only) → GSA/Admin Approval →
Payment Method Selection → ACH Batch / Huntington Transfer →
Ledger Recording → Notify Parties

Interactions With Other Modules

  • Creates LedgerTransaction entries on approval and payment
  • Triggers ACH batch creation (Banking module)
  • Can be linked to a Compensation/SPC (Scheduled Payment Contract) line
  • File attachments stored via Document module (S3)
  • Email notifications sent via SendGrid on state changes

Legacy Issues & Anti-patterns

  • Approval state is tracked via multiple boolean flags (is_approved, is_denied, is_pending_review) rather than a single state field — easy to get into contradictory states
  • Auto-approval logic is duplicated between Flask and FastAPI
  • The general_frontend.py Flask view handles some DR actions that logically belong in a DR-specific module

Gotchas for New Developers

  • “GSA” in the context of DRs refers to the Gestational Surrogacy Agreement signing threshold, not a person — it controls whether CM review is required
  • Auto-approval is a per-case setting that bypasses human review. It is not obvious from the UI when a case has it enabled.
  • DR types (base compensation, lost wages, medical, etc.) each have different approval rules and required attachments — the type field is critical

Module 3 — Scheduled Payments / Compensation (SPC)

Purpose & Business Value

When an Intended Parent signs the surrogacy contract, a payment calendar is created. These are the Scheduled Payment Contract (SPC) items — predictable, recurring payments the surrogate is entitled to at specific milestones (e.g., monthly base compensation, embryo transfer bonuses, delivery bonuses). The SPC module manages this calendar and links it to the DR workflow so expected payments can be auto-initiated.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/compensation.py
FastAPI moduleseedtrustapi/src/seedtrust/modules/case/compensations/
Next.js componentapp/src/app/(authenticated)/case/[caseId]/components/ScheduledPayments.tsx

Main Flows

  1. Admin creates compensation groupings and line items when case enters post-GSA
  2. SPC items have amounts, due dates, and types
  3. When an SPC item comes due, a DR can be created (manually or auto-triggered by cron)
  4. Changes to the SPC are tracked via SPCChange model for audit purposes

Gotchas for New Developers

  • SPC changes create historical records — you cannot simply edit an SPC item and expect the history to be clean. Always look at the change log.
  • The connection between an SPC item and the DR it generates is not always a clean foreign key — some legacy cases have orphaned relationships.

Module 4 — ACH Processing & Banking

Purpose & Business Value

Once a DR is approved, the money must physically move from the escrow account to the surrogate’s bank account. SeedTrust does this via ACH (Automated Clearing House) — the US electronic bank transfer network. This module handles the entire lifecycle: collecting banking details, generating NACHA-format files, submitting them, and tracking results.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/banking.py
Flask modelseedtrust_flask/seedtrust/models/ach.py
FastAPI moduleseedtrustapi/src/seedtrust/modules/banking/
FastAPI Huntington sub-moduleseedtrustapi/src/seedtrust/modules/banking/huntington/
FastAPI ACH formseedtrustapi/src/seedtrust/modules/ach_form/

Main Flows

Party provides bank account/routing number (ACH Form) →
Admin reviews and verifies ACH form →
Approved DRs batched together (NachaBatch) →
NACHA file generated →
File uploaded via SFTP to bank →
ACH return/rejection handling

Huntington Integration

Huntington Bank provides “managed accounts” — SeedTrust holds escrow funds in Huntington accounts. When IPs fund a case, a wire comes into the Huntington account. When surrogates are paid, a disbursement goes out. This is separate from (and more complex than) standard ACH.

Interactions With Other Modules

  • Consumes ACH forms (encrypted banking details per party)
  • Consumes approved DRs to build batches
  • Updates LedgerTransaction records with payment status

Legacy Issues & Anti-patterns

  • NACHA file generation logic is scattered between app_flask.py, banking.py, and a cron job in app_cron.py
  • Error handling for ACH returns (when a bank rejects a payment) is inconsistent between Flask and FastAPI code paths
  • Huntington API integration uses mTLS certificates — the cert management is not documented and lives in environment variables

Gotchas for New Developers

  • ACH forms are encrypted at rest — you cannot read the raw account number from the database without decryption. Flask uses its own encryption utility; FastAPI has its own.
  • NACHA file format is extremely strict — column positions, record lengths, and batch balancing rules must all be exact. A single wrong byte rejects the entire file.
  • Huntington has two separate environments (sandbox and production) with different credentials and different API behaviors. Always confirm which environment you’re targeting.

Module 5 — Ledger & Transaction History

Purpose & Business Value

Every dollar that moves through SeedTrust is recorded in the ledger. The ledger is the financial source of truth — it generates the escrow balance, transaction history, and audit trail that Intended Parents, surrogates, and regulators rely on.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/transactions.py
Flask utilityseedtrust_flask/seedtrust/transaction_history.py
FastAPI moduleseedtrustapi/src/seedtrust/modules/case/transactions/
Next.js componentapp/src/app/(authenticated)/case/[caseId]/components/LedgerStatusView.tsx

Key Models

  • LedgerTransaction: A single financial event (deposit, disbursement, fee)
  • LedgerTransfer: Movement between two ledger accounts
  • DepositNotification: Record of incoming wire deposits

Gotchas for New Developers

  • The ledger is append-only by convention (not by database enforcement). Never update or delete ledger entries — always create correction entries.
  • Balances are computed from the ledger at query time, not stored as a cached field. Heavy reporting queries can be slow on large cases.

Module 6 — User Roles & Permissions

Purpose & Business Value

SeedTrust serves six distinct user types with completely different views, permissions, and capabilities. Getting this wrong means the wrong person sees financial data they shouldn’t, or an approval step is bypassed. The permission system is the security backbone of the platform.

Key Files & Entry Points

LayerLocation
Flask modelsseedtrust_flask/seedtrust/models/admin_permissions.py
Flask auth utilitiesseedtrust_flask/seedtrust/utils/auth.py
Flask permission extensionseedtrust_flask/seedtrust/permission_extension.py
FastAPI dependenciesseedtrustapi/src/seedtrust/dependencies.py
FastAPI enumsseedtrustapi/src/seedtrust/enums.py

User Types

RoleWhat They Do
AdminSeedTrust staff. Has granular permissions (view cases, approve DRs, manage banking).
Agency OwnerRuns a surrogacy/egg donation agency. Sees all cases from their agency.
Case Manager (CM)Assigned to specific cases. Reviews DRs before GSA approval.
Intended Parent (IP)Funds the escrow. Views case status and payment history.
IP RepresentativeAttorney or representative acting on behalf of IPs.
Surrogate/DonorReceives payments. Submits DRs. Manages banking details.

Legacy Issues & Anti-patterns

  • Flask permissions are checked inconsistently: some routes use decorators, others use inline if current_user.is_admin checks — easy to miss a check
  • Admin permissions (AdminPermission, AdminRole) are a custom-built RBAC system. It’s granular but undocumented — you must read the database to understand what permissions exist
  • FastAPI uses a cleaner dependency injection model but still partially mirrors Flask’s permission logic

Gotchas for New Developers

  • Admin and AgencyAdmin are different things. An Admin is a SeedTrust employee. An AgencyAdmin is someone at an agency with admin rights within their agency. Never confuse these.
  • The same email address can exist as multiple user types (e.g., someone who is both an IP and a consultant on different cases). The user_type field on login selects which role is active.

Module 7 — Messaging & Notifications

Purpose & Business Value

Parties communicate about cases through an in-platform inbox. Admins and case managers can open message threads; IPs, surrogates, and agencies can reply. Notifications (push, email, SMS) alert parties to action items — pending DRs, new messages, required uploads.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/message.py
FastAPI moduleseedtrustapi/src/seedtrust/modules/message/
FastAPI notificationsseedtrustapi/src/seedtrust/modules/notification/
Next.js pageapp/src/app/(authenticated)/notifications/

Interactions

  • Push notifications via WebPush (VAPID keys)
  • Email via SendGrid
  • SMS via Twilio
  • Message threads scoped to cases and user types

Module 8 — Document Management

Purpose & Business Value

SeedTrust handles legally significant documents: escrow agreements, lost wages documentation, DR receipts, identification documents. All are stored in AWS S3 and referenced in the database with metadata.

Key Files & Entry Points

LayerLocation
Flask modelseedtrust_flask/seedtrust/models/files.py
FastAPI AWS utilityseedtrustapi/src/seedtrust/core/aws.py
Flask MIME utilityseedtrust_flask/seedtrust/mimetypes.py
Next.js componentapp/src/components/FileSelector.tsx
Escrow agreement moduleseedtrustapi/src/seedtrust/modules/case/escrow_agreement/

Gotchas for New Developers

  • Documents are served via CloudFront signed URLs — they expire. Never hard-code a CloudFront URL; always regenerate via the API.
  • The escrow agreement has a signature flow. The document metadata in the DB tracks whether parties have signed. The PDF itself is in S3.

Module 9 — Authentication & Session Management

Purpose & Business Value

Three separate auth systems are in play, one per service layer. They must stay synchronized because all three share the same user records in MySQL.

Key Files & Entry Points

LayerLocation
Flask authseedtrust_flask/seedtrust/views/auth.py
FastAPI JWT authseedtrustapi/src/seedtrust/modules/user/auth/
FastAPI dependenciesseedtrustapi/src/seedtrust/dependencies.py
Next-Auth configapp/src/auth.ts
Next.js login pageapp/src/app/(auth)/login/

Auth Flows by Service

ServiceMethodNotes
FlaskSession cookie (Flask-Login)Also supports TOTP 2FA
FastAPIStateless JWT30-min access token + refresh token. Cookie or Bearer.
Next.jsNext-Auth CredentialsCalls FastAPI /login, stores JWT in secure session cookie. Auto-refreshes.

Gotchas for New Developers

  • A user changing their password in Flask does NOT invalidate their FastAPI JWT. Token invalidation is not fully synchronized between services.
  • The “user type” must be selected at login — the same email can map to different user records depending on role. The Next.js login form has a role selector for this reason.

Module 10 — Reporting & Analytics

Purpose & Business Value

SeedTrust staff need operational visibility: which cases are active, which payments are pending, cash flow summaries, and audit trails.

Key Files & Entry Points

LayerLocation
Flask viewsseedtrust_flask/seedtrust/views/reports.py (~88KB)
Flask statsseedtrust_flask/seedtrust/seedtruststats.py
External analyticsPostHog (session replay, event tracking)

Gotchas for New Developers

  • reports.py at ~88KB is one of the largest and most brittle files. It contains complex SQL aggregations built inline. Modifying it without understanding the full query context risks breaking financial reports.

Module 11 — Background Jobs (Cron)

Purpose & Business Value

Many platform operations are time-triggered rather than user-triggered: sending payment reminders, generating ACH batches at end of day, auto-initiating SPC payments, expiring stale records.

Key Files & Entry Points

LayerLocation
Flask cronseedtrust_flask/app_cron.py (~66KB)
FastAPI job moduleseedtrustapi/src/seedtrust/modules/job/

Gotchas for New Developers

  • app_cron.py at 66KB is entirely procedural — no structure, no separation of concerns. If a cron job fails silently, there is no automatic alerting.
  • Some cron jobs modify financial data (ACH batching, SPC initiation). A failed job mid-execution can leave data in an inconsistent state.

Module 12 — CLI & Development Environment

Purpose & Business Value

The seedtrust_cli tool (invoked as st) is the onboarding entry point for developers. It handles Python version management, dependency installation, database startup, and service orchestration.

Key Files & Entry Points

LayerLocation
CLI entryseedtrust_cli/src/seedtrust_cli/__init__.py
Run commandseedtrust_cli/src/seedtrust_cli/run/run.py
DB commandseedtrust_cli/src/seedtrust_cli/db/db.py

Available Commands

CommandWhat It Does
st setupFull project bootstrap (installs Python via UV, all dependencies)
st run devStart all three services concurrently
st run flaskStart Flask on :5001
st run fastapiStart FastAPI on :8000
st run nextjsStart Next.js on :3002
st db startStart MySQL via Docker
st db stopStop MySQL container

Section 3: Phased Documentation Roadmap

Writing principle: Document business logic and workflows, not framework-specific implementation details. The domain (what a DR is, how a case works, the ACH flow) guides the Flask consolidation. Framework-specific FastAPI, Next.js, and new-st details should not become new long-term dependencies.


Phase 1 — Quick Wins (Weeks 1–2)

Goal: Unblock a new developer from being productive within their first day.

DocumentWhy It Matters
docs/getting-started.mdStep-by-step: clone → st setupst db startst run dev → open each service. No assumptions.
docs/architecture-review.mdThe Mermaid diagram above plus a plain-English explanation of how the three services relate. Answers “where does code live?”
docs/business-glossary.mdDefine every domain term: Case, DR, SPC, GSA, IP, CM, Surrogate, NACHA, ACH, Huntington, Escrow. Non-technical stakeholders and new engineers both need this.
docs/user-roles.mdWho can do what. Which routes are gated to which roles. New engineers touching auth or permissions need this immediately.

Why this phase first: Without a glossary, a developer cannot read the code. Without the architecture overview, they can’t navigate the repo.


Phase 2 — Core Workflows (Weeks 3–5)

Goal: Enable a developer to make a change to an existing workflow without breaking it.

DocumentWhy It Matters
docs/modules/disbursement-requests.mdThe DR lifecycle is the most-changed, most-broken feature. Document every state, every transition, every approval rule.
docs/modules/ach-banking.mdACH errors are financially catastrophic and hard to debug. Document: NACHA format basics, Huntington vs. standard ACH, encrypted ACH forms, error handling.
docs/modules/case-lifecycle.mdEvery stage transition, what it enables/disables, pre-GSA vs. post-GSA differences.
docs/modules/auth-and-permissions.mdHow Flask, FastAPI, and Next-Auth differ. How to add a new permission. Common mistakes.
docs/migration-guide.md”Should this feature go in Flask or FastAPI?” Practical rules, not just “FastAPI for new stuff.”

Phase 3 — Full Coverage (Weeks 6–10)

Goal: A developer can onboard to any area of the codebase without needing to ask a teammate.

DocumentWhy It Matters
docs/modules/ledger-transactions.mdAppend-only rules, how balances are calculated, how to read the ledger. Prevents accidental data corruption.
docs/modules/scheduled-payments.mdSPC structure, change tracking, link to DRs.
docs/modules/document-management.mdS3 uploads, CloudFront signed URLs, escrow agreement signing flow.
docs/modules/messaging-notifications.mdPush, email, SMS — when each fires, how to test locally.
docs/modules/reporting.mdWhere reports live, known performance issues, how to safely add a new report.
docs/modules/background-jobs.mdEvery cron job, its schedule, what it does, what breaks if it fails.
docs/frontend/nextjs-overview.mdRoute structure, auth flow, how React Query is used, shadcn/ui patterns.
docs/frontend/api-integration.mdHow Next.js calls FastAPI, token refresh flow, error handling patterns.
docs/runbooks/ach-batch-generation.mdHow to generate and submit an ACH batch manually. Operational runbook.
docs/runbooks/adding-permissions.mdHow to add a new Admin permission to the RBAC system.
docs/adr/Architecture Decision Records: why FastAPI was added, why Huntington was chosen, and why the plan changed from Go/SvelteKit rewrite to Flask-only consolidation. Prevents future teams from re-debating settled decisions.

Practices for Future Code Changes

Document at the decision point, not after. When you make a non-obvious choice (why a payment bypasses the normal approval flow, why a NACHA field is hardcoded), write a one-line code comment with a # WHY: prefix and add a note to the relevant module doc. Decisions are cheapest to document when the context is fresh.

Write business-first, not code-first. The domain survives framework consolidation. Prioritize documenting what a workflow does and why it exists over how it is implemented today, then use that understanding to move ownership toward Flask.

Treat app_flask.py and app_cron.py as hazmat. Both are 66–400KB monoliths. When adding to Flask, add to a blueprint in views/ and import it — do not add to the monolith files directly. When fixing a cron job, extract it to a named function with a docstring before editing.

Keep the glossary current. The business domain evolves (new payment types, new case statuses, regulatory requirements). Assign one person to own docs/business-glossary.md and review it quarterly.

Write an ADR before any integration change. Huntington, NACHA, Stripe, Alacriti — changes to external integrations have financial consequences. A brief Architecture Decision Record (problem, options considered, decision, consequences) makes it possible to revisit the decision safely.


Targeted Follow-Up Questions

These areas have high value but require deeper file-level exploration to document accurately:

  1. “Walk me through the full DR approval state machine” — read disbursement_request.py + approval routes in cm.py and owner.py to build the definitive state diagram
  2. “Map every Admin permission that exists” — enumerate the full permission list from the model/seed data for user-roles.md
  3. “Document the Huntington account lifecycle” — read banking/huntington/ in FastAPI and the Huntington sections of app_flask.py to produce the operational runbook
  4. “What does app_cron.py actually schedule?” — read the full cron file to produce the job inventory table
  5. “How does the Next.js token refresh work end-to-end?” — read auth.ts + the FastAPI refresh endpoint to draw the exact sequence diagram for the frontend auth doc