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 --> PostHogSection 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
| Layer | Location |
|---|---|
| Flask model | seedtrust_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 service | seedtrustapi/src/seedtrust/modules/case/service.py |
| FastAPI route | seedtrustapi/src/seedtrust/modules/case/route.py |
| Next.js page | app/src/app/(authenticated)/case/[caseId]/page.tsx |
Main Flows
- Admin creates a case → links IP, surrogate, agency → sets case type (surrogacy vs. egg donation) and contract stage
- Case moves through stages:
inquiry → pre-GSA → post-GSA → closed - GSA (Gestational Surrogacy Agreement) signing marks the legal threshold — before it, payment rules are more restricted
- 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.pyandowner.pyare 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.pyis a 400KB+ file that registers all routes and contains ad-hoc logic — it is the primary “gotcha” file
Gotchas for New Developers
- The
ip_casetable is the database table name; the Python class isCase. 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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/disbursement_request.py |
| FastAPI module | seedtrustapi/src/seedtrust/modules/case/disbursement_requests/ |
| Auto-approver | seedtrustapi/src/seedtrust/modules/auto_approver/ |
| Next.js component | app/src/app/(authenticated)/case/[caseId]/components/DisbursementRequests.tsx |
| Next.js add flow | app/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 PartiesInteractions 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.pyFlask 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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/compensation.py |
| FastAPI module | seedtrustapi/src/seedtrust/modules/case/compensations/ |
| Next.js component | app/src/app/(authenticated)/case/[caseId]/components/ScheduledPayments.tsx |
Main Flows
- Admin creates compensation groupings and line items when case enters post-GSA
- SPC items have amounts, due dates, and types
- When an SPC item comes due, a DR can be created (manually or auto-triggered by cron)
- Changes to the SPC are tracked via
SPCChangemodel 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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/banking.py |
| Flask model | seedtrust_flask/seedtrust/models/ach.py |
| FastAPI module | seedtrustapi/src/seedtrust/modules/banking/ |
| FastAPI Huntington sub-module | seedtrustapi/src/seedtrust/modules/banking/huntington/ |
| FastAPI ACH form | seedtrustapi/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 handlingHuntington 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 inapp_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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/transactions.py |
| Flask utility | seedtrust_flask/seedtrust/transaction_history.py |
| FastAPI module | seedtrustapi/src/seedtrust/modules/case/transactions/ |
| Next.js component | app/src/app/(authenticated)/case/[caseId]/components/LedgerStatusView.tsx |
Key Models
LedgerTransaction: A single financial event (deposit, disbursement, fee)LedgerTransfer: Movement between two ledger accountsDepositNotification: 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
| Layer | Location |
|---|---|
| Flask models | seedtrust_flask/seedtrust/models/admin_permissions.py |
| Flask auth utilities | seedtrust_flask/seedtrust/utils/auth.py |
| Flask permission extension | seedtrust_flask/seedtrust/permission_extension.py |
| FastAPI dependencies | seedtrustapi/src/seedtrust/dependencies.py |
| FastAPI enums | seedtrustapi/src/seedtrust/enums.py |
User Types
| Role | What They Do |
|---|---|
| Admin | SeedTrust staff. Has granular permissions (view cases, approve DRs, manage banking). |
| Agency Owner | Runs 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 Representative | Attorney or representative acting on behalf of IPs. |
| Surrogate/Donor | Receives 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_adminchecks — 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
AdminandAgencyAdminare different things. AnAdminis a SeedTrust employee. AnAgencyAdminis 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_typefield 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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/message.py |
| FastAPI module | seedtrustapi/src/seedtrust/modules/message/ |
| FastAPI notifications | seedtrustapi/src/seedtrust/modules/notification/ |
| Next.js page | app/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
| Layer | Location |
|---|---|
| Flask model | seedtrust_flask/seedtrust/models/files.py |
| FastAPI AWS utility | seedtrustapi/src/seedtrust/core/aws.py |
| Flask MIME utility | seedtrust_flask/seedtrust/mimetypes.py |
| Next.js component | app/src/components/FileSelector.tsx |
| Escrow agreement module | seedtrustapi/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
| Layer | Location |
|---|---|
| Flask auth | seedtrust_flask/seedtrust/views/auth.py |
| FastAPI JWT auth | seedtrustapi/src/seedtrust/modules/user/auth/ |
| FastAPI dependencies | seedtrustapi/src/seedtrust/dependencies.py |
| Next-Auth config | app/src/auth.ts |
| Next.js login page | app/src/app/(auth)/login/ |
Auth Flows by Service
| Service | Method | Notes |
|---|---|---|
| Flask | Session cookie (Flask-Login) | Also supports TOTP 2FA |
| FastAPI | Stateless JWT | 30-min access token + refresh token. Cookie or Bearer. |
| Next.js | Next-Auth Credentials | Calls 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
| Layer | Location |
|---|---|
| Flask views | seedtrust_flask/seedtrust/views/reports.py (~88KB) |
| Flask stats | seedtrust_flask/seedtrust/seedtruststats.py |
| External analytics | PostHog (session replay, event tracking) |
Gotchas for New Developers
reports.pyat ~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
| Layer | Location |
|---|---|
| Flask cron | seedtrust_flask/app_cron.py (~66KB) |
| FastAPI job module | seedtrustapi/src/seedtrust/modules/job/ |
Gotchas for New Developers
app_cron.pyat 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
| Layer | Location |
|---|---|
| CLI entry | seedtrust_cli/src/seedtrust_cli/__init__.py |
| Run command | seedtrust_cli/src/seedtrust_cli/run/run.py |
| DB command | seedtrust_cli/src/seedtrust_cli/db/db.py |
Available Commands
| Command | What It Does |
|---|---|
st setup | Full project bootstrap (installs Python via UV, all dependencies) |
st run dev | Start all three services concurrently |
st run flask | Start Flask on :5001 |
st run fastapi | Start FastAPI on :8000 |
st run nextjs | Start Next.js on :3002 |
st db start | Start MySQL via Docker |
st db stop | Stop 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-stdetails 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.
| Document | Why It Matters |
|---|---|
docs/getting-started.md | Step-by-step: clone → st setup → st db start → st run dev → open each service. No assumptions. |
docs/architecture-review.md | The Mermaid diagram above plus a plain-English explanation of how the three services relate. Answers “where does code live?” |
docs/business-glossary.md | Define 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.md | Who 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.
| Document | Why It Matters |
|---|---|
docs/modules/disbursement-requests.md | The DR lifecycle is the most-changed, most-broken feature. Document every state, every transition, every approval rule. |
docs/modules/ach-banking.md | ACH 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.md | Every stage transition, what it enables/disables, pre-GSA vs. post-GSA differences. |
docs/modules/auth-and-permissions.md | How 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.
| Document | Why It Matters |
|---|---|
docs/modules/ledger-transactions.md | Append-only rules, how balances are calculated, how to read the ledger. Prevents accidental data corruption. |
docs/modules/scheduled-payments.md | SPC structure, change tracking, link to DRs. |
docs/modules/document-management.md | S3 uploads, CloudFront signed URLs, escrow agreement signing flow. |
docs/modules/messaging-notifications.md | Push, email, SMS — when each fires, how to test locally. |
docs/modules/reporting.md | Where reports live, known performance issues, how to safely add a new report. |
docs/modules/background-jobs.md | Every cron job, its schedule, what it does, what breaks if it fails. |
docs/frontend/nextjs-overview.md | Route structure, auth flow, how React Query is used, shadcn/ui patterns. |
docs/frontend/api-integration.md | How Next.js calls FastAPI, token refresh flow, error handling patterns. |
docs/runbooks/ach-batch-generation.md | How to generate and submit an ACH batch manually. Operational runbook. |
docs/runbooks/adding-permissions.md | How 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:
- “Walk me through the full DR approval state machine” — read
disbursement_request.py+ approval routes incm.pyandowner.pyto build the definitive state diagram - “Map every Admin permission that exists” — enumerate the full permission list from the model/seed data for
user-roles.md - “Document the Huntington account lifecycle” — read
banking/huntington/in FastAPI and the Huntington sections ofapp_flask.pyto produce the operational runbook - “What does
app_cron.pyactually schedule?” — read the full cron file to produce the job inventory table - “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