How a Route Works¶
Every HTTP request in PaperRun follows the same layer architecture:
Route -> Method -> Model -> Core
- Routes (
app/routes/) — Handle HTTP concerns: request validation, deserialization, auth checks, response serialization. Routes call methods, never integrations directly. - Methods (
app/methods/) — Business logic. Orchestrate model calls, apply rules, coordinate between models. This is where domain logic lives. - Models (
app/core/models/) — Database access. Each model file owns the SQL for its table — inserts, updates, queries. No business logic. - Core (
app/core/integrations/,app/core/utils/) — External service clients and shared utilities. Called by methods, never by routes.
Concrete Trace: Creating a Campaign¶
Here's what happens when a merchant creates a new campaign, traced through real files:
sequenceDiagram
participant Client
participant Route as campaign_routes.py
participant Method as generator.py
participant Model as campaigns.py (model)
participant DB as PostgreSQL
participant Celery as Beat Scheduler
Client->>Route: POST /api/v1/campaigns
Route->>Route: Validate auth (is_user_authorized_for_organization)
Route->>Route: Deserialize request body into Campaign object
Route->>Model: create_campaign(campaign)
Model->>DB: INSERT INTO campaigns (...) RETURNING campaign_id
DB-->>Model: campaign_id
Model->>Model: get_campaign(campaign_id)
Model-->>Route: Campaign
Route->>Route: Serialize to JSON response
alt autogenerate=True
Route->>Method: autogenerate_campaign_items(campaign)
end
alt has provider_segment_id
Route->>Route: submit_campaign_sync_request()
end
Route-->>Client: 201 Created + Campaign JSON
Note over Celery: Every 15 min, beat tasks<br/>poll for Active campaigns
Celery->>Model: list_automation_campaigns([Active])
Celery->>Celery: Dispatch recipient sync per campaign
1. Route: app/routes/campaign_routes.py¶
The POST handler at create_campaign() (line ~211):
- Checks user authorization via om.is_user_authorized_for_organization()
- Creates a Campaign object from the request body
- Calls c.create_campaign(campaign) — the model layer
- If autogenerate=True, calls autogenerate_campaign_items() to set up templates
- If the campaign has a provider_segment_id, enqueues an async recipient sync via submit_campaign_sync_request()
The route handles HTTP concerns only — no business logic about what makes a valid campaign.
2. Model: app/core/models/campaigns.py¶
The create_campaign(campaign) function (line ~609):
- Opens a DB connection via get_db_client()
- Executes a raw SQL INSERT with the campaign fields
- Calls get_campaign(campaign_id) to fetch and return the fully hydrated Campaign object
Key types defined here:
- CampaignStatus enum — Pending, Active, Completed, Archived, Scheduled, Paused, Error
- Campaign.__init__(obj: dict) — Constructor that hydrates from a database row
- Campaign.to_json() — Serialization for API responses
3. How It Enters Automation¶
There is no explicit "start" trigger. Once a campaign exists with Active status, Celery beat tasks automatically pick it up:
process_automated_recipient_sync_campaignsruns every 15 minutes inapp/celery/automator.py- It queries for all automation campaigns with status in [Pending, Active, Paused], grouped by organization
- For each organization, it dispatches
sync_recipients_for_org_campaigns.apply_async(), which then syncs each of that org's campaigns sequentially in-process. Cross-org work runs in parallel; within an org it is serial to preserve cross-campaign recipient exclusion.
This is the handoff from synchronous request handling to the background automation pipeline. From here, the recipient lifecycle takes over — see Recipient Pipeline.
The Pattern¶
Every route in the codebase follows this same structure: 1. Auth check 2. Deserialize request 3. Call method or model 4. Serialize response
Methods add orchestration when the route needs to coordinate multiple models or integrations. Simple CRUD routes (like campaign creation) can call models directly. Complex operations (like proofing or attribution) always go through methods.
For canonical REST response shapes, including the required pagination object for new paginated endpoints, see API Conventions.
For how background automation works after this point, see: - Recipient Pipeline — Sync -> proof -> send - Order Syncing — Attribution pipeline