Project structure

One repository holds the API, the dashboard, the React SDK, and the Workflows tasks. This page maps what lives where.

The repository

It's a pnpm workspace with Next.js apps and one Node package. The API sits beside them on Bun with its own lockfile, and runs in Docker.

  • api/Offer API: Hono on Bun, Postgres. Not in the pnpm workspace.
  • src/routes/One file per resource
  • src/lib/Auth, plan resolution, PayPal, webhooks, cancel flows, AI offers
  • src/mcp/Each app's MCP server: protocol, tools, resources and prompts
  • migrations/SQL migrations, run on boot
  • test/End-to-end tests against a real database
  • offer-app/The dashboard (Next.js), plus the React SDK source
  • src/app/Dashboard pages, the BFF and the agent route
  • src/sdk/React SDK: core, checkout/ and cancel/
  • src/server/API client, auth, the agent's tools and the mock API
  • workflows/Render Workflow tasks for subscription pauses
  • render.yamlRender Blueprint for the whole stack
  • docker-compose.ymlLocal Postgres and API

Services

A deploy runs three services and a database. Each one has a single job.

ServiceCodeJob
offersdk-apiapi/Stores every record, resolves access, prices checkouts with PayPal, runs cancel sessions, signs and delivers webhooks. Also hosts dashboard sign-in.
offer-appoffer-app/The dashboard your team uses. It has no database of its own: every read and write goes to the API.
offersdk-workflowsworkflows/Pauses and resumes PayPal subscriptions as Render Workflow tasks, with retries per account.
offersdk-dbPostgres 17Records as JSONB documents, plus usage counters, events, checkouts and auth tables.
Your app
React SDK in the browser, your server with the secret key
Dashboard
offer-app (Next.js). Its BFF calls the API as admin
Render Workflows
offersdk-workflows: subscription pauses
Offer API
offersdk-api (Hono on Bun). Holds every record, signs webhooks, prices checkouts
Postgres
offersdk-db
PayPal
Orders, subscriptions, webhooks
AI model
Dynamic save offers
Webhooks
Your endpoints and Zapier
The dashboard also calls the AI model directly for its agent chat, and proxies sign-in to the API so session cookies stay on its own domain.

How a request moves

From your app

Your server calls the API with the app's secret key. The browser calls it with the publishable key, or with a short-lived account token for flows that act for one customer, such as cancelling. Every app route lives under /apps/:appId.

From the dashboard

The browser never holds an API key. Dashboard pages call the dashboard's own server at /api/admin/*, which checks the session, checks that the app belongs to the signed-in workspace, and forwards the call to the API with the admin key. Sign-in works the same way: /api/auth/* is proxied to the API, so session cookies are set on the dashboard's domain.

From PayPal and Render

PayPal posts to /paypal/webhooks/:appId on the API, which verifies each notification with PayPal before acting on it. Workflow tasks call the API's admin-only /pauses routes to apply and lift pauses.

Mock mode

The dashboard also ships an in-memory copy of the API in offer-app/src/server/offer-api/mock, with the same routes, status codes and key rules. Set OFFER_API_URL=mock to use it. It has no offers, checkouts, PayPal or cancel flows, so those sections are hidden. It has no MCP server either, so the MCP page shows a preview with sample data. Its data resets when the server restarts.

The SDK isn't on npm. It lives in offer-app/src/sdk and has three entry points: @offer/sdk, @offer/sdk/checkout and @offer/sdk/cancel. Another app imports them through tsconfig paths. See Access and entitlements.

Tests

  • API. bun test in api/ runs end-to-end against a real database and truncates every table, so point DATABASE_URL at a separate test database. PayPal is faked.
  • Dashboard. pnpm --filter offer-app test runs Vitest, including tests of the mock API's own routes, status codes and key rules.