Skip to content

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_campaigns runs every 15 minutes in app/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