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.
| Service | Code | Job |
|---|---|---|
| offersdk-api | api/ | Stores every record, resolves access, prices checkouts with PayPal, runs cancel sessions, signs and delivers webhooks. Also hosts dashboard sign-in. |
| offer-app | offer-app/ | The dashboard your team uses. It has no database of its own: every read and write goes to the API. |
| offersdk-workflows | workflows/ | Pauses and resumes PayPal subscriptions as Render Workflow tasks, with retries per account. |
| offersdk-db | Postgres 17 | Records as JSONB documents, plus usage counters, events, checkouts and auth tables. |
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.
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 testinapi/runs end-to-end against a real database and truncates every table, so pointDATABASE_URLat a separate test database. PayPal is faked. - Dashboard.
pnpm --filter offer-app testruns Vitest, including tests of the mock API's own routes, status codes and key rules.