Migration Guide
This guide answers the most common question developers face when adding or changing a feature: where does this code belong? The current answer is intentionally simpler than earlier plans: move SeedTrust toward one Flask application, not a Go/SvelteKit rewrite or a larger FastAPI surface.
The Current State
SeedTrust is mid-migration. Three production systems run today, plus a Go/SvelteKit shell/experiment, but the target is a single Flask application:
| Service | Status | Rule |
|---|---|---|
Flask (seedtrust_flask) | Production — strategic home | New product/backend work and consolidation land here |
FastAPI (seedtrustapi) | Production — mobile API/support code | Maintain existing behavior; backfill useful APIs into Flask over time |
Next.js (app) | Production — mobile PWA | Maintain only where needed while flows move into Flask |
new-st (new-st/) | Go/SvelteKit shell/experiment | Not the strategic target; do not expand unless explicitly needed |
The previous Go/SvelteKit rewrite plan has been superseded. The product direction is simplicity: one codebase, fewer moving parts, and a smaller tech stack centered on Flask. Dynamic UI should generally use server-rendered Jinja pages enhanced with Alpine/HTMX, with JSON endpoints feeding partials where data needs to refresh without a full page load.
Decision Tree: Where Does This Code Go?
Is this a bug fix on existing Flask behavior? └─ YES → Fix it in Flask. └─ NO ↓
Is this a new feature or enhancement? └─ Can it be built directly in Flask with Jinja + Alpine/HTMX + JSON/partials? └─ YES → Flask └─ NO → Ask before expanding another codebase
Is this needed only to keep the current mobile PWA stable? └─ YES → Minimal change in FastAPI/Next.js, plus note the Flask consolidation path └─ NO ↓
Is this a background job? └─ Does it already exist in app_cron.py? └─ YES → Fix or refactor in Flask └─ NO → Prefer Flask-owned job code unless explicitly tied to existing FastAPI infrastructureDefault rule: build toward Flask-only. When in doubt, ask how the change reduces codebase count, auth split, and framework complexity.
What Lives Where Today
Flask Owns
- All desktop admin UI (Jinja2 templates, server-rendered HTML)
- NACHA batch generation and SFTP delivery
- Legacy banking operations (M&T path)
- E-check processing
- PDF generation and document rendering
- Flask-Login session management
- Existing cron jobs (
app_cron.py) - Reporting pages (
reports.py)
FastAPI Owns
- All mobile API endpoints (consumed by Next.js)
- Huntington Bank integration
- JWT authentication and token refresh
- ACH form management (read/write)
- Case listing and detail (for mobile)
- Disbursement request creation and approval (for mobile)
- Push notifications
- Messaging (for mobile)
- Background worker (Huey)
- Agency webhooks (public API)
Both Own (Duplicate Logic — Known Issue)
- Auto-approval logic — exists in both, should be consolidated toward Flask
- Case data queries — both services read from the same tables
- Email sending — both call SendGrid directly
How to Add a New FastAPI Endpoint
Avoid adding new FastAPI endpoints unless they are required to keep the current mobile PWA or external integrations stable during consolidation. Prefer a Flask route or JSON endpoint that can serve a Jinja partial through HTMX. If a temporary FastAPI endpoint is unavoidable, use the modular structure below and document the Flask consolidation path.
FastAPI uses a modular structure. Each feature lives in seedtrustapi/src/seedtrust/modules/<feature>/.
Module Structure
modules/└── my_feature/ ├── route.py # FastAPI router + endpoint definitions ├── service.py # Business logic (queries, transformations) └── models.py # Pydantic request/response schemasThe router in route.py is auto-discovered — add a router object and it is registered automatically at startup.
Minimal Example
modules/my_feature/models.py
from pydantic import BaseModel
class MyFeatureResponse(BaseModel): id: int name: strmodules/my_feature/service.py
from sqlalchemy import selectfrom sqlalchemy.ext.asyncio import AsyncSessionfrom seedtrust.models.case import Case
async def get_my_data(db: AsyncSession, case_id: int) -> dict: result = await db.execute( select(Case).where(Case.case_id == case_id) ) case = result.scalar_one_or_none() return {"id": case.case_id, "name": case.case_file_name}modules/my_feature/route.py
from fastapi import APIRouterfrom seedtrust.dependencies import CurrentUser, DbSessionfrom .models import MyFeatureResponsefrom .service import get_my_data
router = APIRouter(prefix="/my-feature", tags=["My Feature"])
@router.get("/{case_id}", response_model=MyFeatureResponse)async def get_feature(case_id: int, current_user: CurrentUser, db: DbSession): return await get_my_data(db, case_id)How to Consolidate an Endpoint into Flask
The preferred migration direction is now toward Flask, not from Flask to FastAPI. Use this process when moving behavior from FastAPI/Next.js/new-st into seedtrust_flask, or when modernizing an existing Flask route without expanding another service.
Step 1 — Understand the Source Behavior Fully
Before writing code, read the existing route or page end-to-end. Legacy views often mix business logic, database queries, permission checks, email sending, and HTML rendering all in one function. FastAPI modules may split those concerns across route.py, service.py, and Pydantic models. Untangle these before consolidating.
Ask:
- What database queries does it make?
- What permissions does it require?
- What does it return — HTML, JSON, or redirect?
- Does it send emails or notifications?
- Does it have side effects (creates ledger entries, triggers background jobs)?
- Does it handle file uploads?
Step 2 — Define the UI/API Contract First
Decide whether the Flask surface should return a full server-rendered page, an HTMX partial, JSON for Alpine state, or a redirect/flash response. Be explicit about the response shape before writing logic.
Step 3 — Extract Business Logic into Flask Services/Helpers
Move database queries and business logic out of templates and oversized view functions where practical. Keep Flask routes focused on HTTP concerns: auth, request parsing, calling service/helper code, rendering templates or partials, and formatting JSON.
Step 4 — Handle Permissions
Flask has the granular Admin permission system. When consolidating behavior into Flask, preserve or add the correct @authcheck(..., perm_check=...) decorator. If a temporary FastAPI endpoint must enforce an Admin permission, query the Flask permission tables manually and document the eventual Flask owner:
async def require_permission(user: CurrentUser, permission: str): if user.user_type != "admin": raise HTTPException(403) # Query AdminRole/AdminPermission manually if the specific permission matters # See docs/modules/auth-and-permissions.mdDocument the required permission in a comment on any temporary FastAPI endpoint, and prefer the Flask decorator once the behavior is consolidated.
Step 5 — Retire the Other Surface Carefully
Once the Flask route, partial, or JSON endpoint is live and the consuming UI has moved:
- Add a deprecation comment to the old FastAPI/Next.js/new-st path:
# DEPRECATED: consolidated into Flask /admin/my-feature# Remove once no mobile/new-st callers remain
- Do not delete the old route until callers and deployments are verified
- Track the deprecation in the relevant module doc’s open questions
What Not to Do
Do not add new product ownership to FastAPI, Next.js, or new-st. New product functionality should move SeedTrust toward Flask as the only application. Temporary changes outside Flask are acceptable only to keep existing production surfaces stable during consolidation.
Pre-existing exception:
seedtrustapi/src/seedtrust/modules/admin_actions/is an admin-only FastAPI module (GET /admin-actions/actions, guarded byUserTypes.ADMIN) that has no frontend consumer yet. Do not build on top of it — it should be migrated or deleted as part of the Flask consolidation path.
Do not copy-paste Flask synchronous ORM code into FastAPI. Flask uses synchronous SQLAlchemy (db.session.query()). FastAPI uses async SQLAlchemy (await db.execute(select(...))). Copy-pasted sync code will block the async event loop.
Do not add new routes to app_flask.py. It is a 400KB monolith. New Flask routes go in a blueprint under seedtrust_flask/seedtrust/views/.
Do not add unstructured cron code to app_cron.py. It is 66KB of procedural code. New background tasks should still be Flask-owned, but place them behind named functions/services and tests rather than expanding the monolith casually.
Do not create migrations manually. The CLAUDE.md rule applies here: do not write your own Alembic migration. Define the model change and let the dev team generate the migration file.
Async SQLAlchemy Patterns
FastAPI uses async SQLAlchemy. The patterns differ from Flask’s sync ORM.
Querying
cases = Case.query.filter_by(active=True).all()
from sqlalchemy import selectresult = await db.execute(select(Case).where(Case.active == True))cases = result.scalars().all()Single Record
result = await db.execute(select(Case).where(Case.case_id == case_id))case = result.scalar_one_or_none()if case is None: raise HTTPException(status_code=404)Creating a Record
new_dr = DisbursementRequest(case_id=case_id, amount=amount, ...)db.add(new_dr)await db.flush() # gets the generated ID without committingawait db.commit()await db.refresh(new_dr) # reload from DB to get all defaultsThe Flask-Only Future
The long-term plan is to simplify SeedTrust into a single primary Flask application. The previous Go/SvelteKit rewrite direction is no longer the target.
What this means for you today:
- New product/backend work should go in
seedtrust_flask - Keep FastAPI and Next.js stable, but avoid expanding them unless the current production surface needs it
- Treat
new-stas a shell/experiment, not the destination for product ownership - Prefer simple server-rendered Jinja pages enhanced with Alpine/HTMX
- Use JSON APIs to fetch data for partials where dynamic updates are needed
- Document business logic in
docs/modules/; those docs guide consolidation and outlive framework choices
What should transfer into Flask:
- Business logic (the what and why)
- Mobile/API-only flows that still matter to the product
- External service integrations that Flask must own long-term (Huntington, NACHA, SendGrid, etc.)
- Permission checks and role definitions, using Flask’s existing RBAC as the source of truth
What should be reduced or removed over time:
- FastAPI-specific patterns where Flask owns the same workflow
- Next.js screens once equivalent Flask flows exist
- Go/SvelteKit shell code that does not simplify the product or support the Flask consolidation path
Open Questions
1. Shared business logic Some logic is currently duplicated between Flask and FastAPI (auto-approval, email sending). What is the safest sequence for making Flask the owner without disrupting existing mobile/PWA behavior?