Open source payment infrastructure platform for developers. Monorepo with a Python/FastAPI backend and a Next.js frontend.
This file is the entry point for AI agents working in this repo: start here, then read the
per-area AGENTS.md linked from the Architecture and Conventions sections before writing code.
- Do not add comments unless necessary — the code should be self-explanatory.
- Use meaningful variable and function names.
- Follow established conventions and good practices (SOLID, maintainable code).
- Do not modify code unrelated to the task or issue you are working on.
polar/
├── server/ # Python/FastAPI backend — see server/AGENTS.md
│ ├── polar/
│ │ ├── {module}/
│ │ │ ├── endpoints.py # FastAPI routes
│ │ │ ├── service.py # Business logic (singleton)
│ │ │ ├── repository.py # Database queries (SQLAlchemy)
│ │ │ ├── schemas.py # Pydantic models
│ │ │ └── tasks.py # Dramatiq background jobs
│ │ ├── models/ # SQLAlchemy models (global, not per-module)
│ │ └── backoffice/ # Admin UI (HTMX + DaisyUI) — see server/polar/backoffice/AGENTS.md
│ └── migrations/ # Alembic database migrations
├── clients/ # Turborepo + pnpm frontend — see clients/AGENTS.md
│ ├── adapters/ # Published framework and authentication adapters
│ ├── apps/web/ # Next.js dashboard
│ ├── apps/app/ # Expo / React Native (iOS + Android)
│ ├── apps/orbit/ # Orbit design-system showcase
│ ├── packages/orbit/ # Orbit design system (components + tokens)
│ ├── packages/ui/ # Legacy shared components (Radix + Tailwind)
│ ├── packages/client/ # Generated API client + data hooks
│ └── packages/i18n/ # Translations
├── dev/ # Dev scripts and tooling
├── docs/ # User/developer docs (Mintlify)
├── sdk/ # SDKs and generators
│ ├── generator/ # Internal SDK code generator
│ ├── python/ # Generated Python SDK
│ └── overlays/ # OpenAPI Overlay tweaks for Speakeasy-generated SDKs
└── .claude/ # Claude Code config (settings, hooks, commands)
The TypeScript API client is generated from the backend's OpenAPI schema. After changing the
API, run pnpm run generate in clients/packages/client.
./dev/setup-environment # generate .env files
# For GitHub integration:
./dev/setup-environment --setup-github-app --backend-external-url https://yourdomain.ngrok.devBackend (http://127.0.0.1:8000) — from server/:
../dev/setup-environment # generates server/.env — must run BEFORE docker compose
docker compose up -d # PostgreSQL, Redis, Minio
uv sync # install deps
uv run task api # API server
uv run task worker # background worker (separate terminal)docker-compose.yml interpolates the Postgres credentials and MinIO bucket names from
server/.env. Without that file they expand to empty strings and the db and minio-setup
containers exit 1 — while docker compose up -d still exits 0. Verify with docker ps -a,
not the exit code. (dev up gets this order right; only the manual sequence above used to not.)
Frontend (http://127.0.0.1:3000) — from clients/:
pnpm install && pnpm devStripe — add to server/.env:
POLAR_STRIPE_SECRET_KEYPOLAR_STRIPE_PUBLISHABLE_KEYPOLAR_STRIPE_WEBHOOK_SECRETPOLAR_STRIPE_CONNECT_WEBHOOK_SECRET
Fresh worktrees (.claude/worktrees/) don't carry .env or built artifacts. Before running
tests in a new worktree, from the repo root:
./dev/setup-environment # generates server/.env, server/.jwks.json, clients/apps/web/.env.local
uv run --directory server task emails # builds server/emails/bin/react-email-pkgWithout these, pytest fails at config load with JWKS and EMAIL_RENDERER_BINARY_PATH errors.
Only two artifacts actually block config import: server/.jwks.json and any existing file at
EMAIL_RENDERER_BINARY_PATH — the validator only checks that the path exists. When you need to
collect tests, lint or typecheck without waiting on the ~60s email build, do what
test_sdk.yaml does: touch /tmp/email-renderer and set
POLAR_EMAIL_RENDERER_BINARY_PATH=/tmp/email-renderer. Tests that render an email will fail,
nothing else will.
Always prefix Python commands with uv run — it guarantees the correct Python (3.14),
project dependencies, environment variables, and virtualenv context.
cd server
uv run task test_fast # backend tests, parallel, no coverage
uv run task lint && uv run task lint_types # lint + type-check
uv run alembic revision --autogenerate -m "description" # generate a migration from model changes
uv run alembic upgrade head # apply migrationsuv run task test adds coverage and runs serially. Use task test_fast (-n auto, no coverage)
interactively, or scope to a path: POLAR_ENV=testing uv run python -m pytest tests/<module>.
Set POLAR_TEST_DATABASE_TEMPLATE=polar_test (with polar_test created and migrated) and each
xdist worker clones that database instead of replaying the whole migration history into its own
— see tests/fixtures/database.py. CI does this in test_server.yaml. If you add a migration,
re-run POLAR_ENV=testing uv run task db_migrate to refresh the template, or workers will
clone a stale schema.
Visual regression testing — use dev snap to capture before/after screenshots across branches:
dev snap --branch my-feature # test a specific branch
dev snap --detect # auto-detect URLs from git diffThe customer portal authenticates with a session token rather than the dashboard login, so
dev snap can't reach it on its own. Get its URLs from dev portal-urls --snap first.
See server/AGENTS.md for backend command and testing specifics.
Detailed, review-enforced patterns live next to the code — read the relevant file before writing:
- Backend →
server/AGENTS.md: modular structure, repository/service/endpoint patterns,lazy="raise"relationships, status-codedPolarError, endpoints return ORM models, authentication (AuthSubject+ scopes). - Frontend →
clients/AGENTS.md: Orbit<Box />design system (raw Tailwind is deprecated for layout/spacing/color/etc.), TanStack Query for data, Zustand for state, 250-linemax-lineslimit. - Backoffice →
server/polar/backoffice/AGENTS.md: HTMX + DaisyUI patterns.
i18n: add new translatable strings only to clients/packages/i18n/src/locales/en.ts — a CI
job auto-translates the rest. Don't edit other locale files. (More in clients/AGENTS.md.)
Significant, cross-cutting, or hard-to-reverse decisions are recorded as short ADRs in
handbook/engineering/decisions/ (see the index).
Treat Accepted ADRs as binding:
- Before changing a load-bearing pattern, check for a relevant ADR (grep that directory).
- If code contradicts an Accepted ADR, flag it and cite the id (e.g. "violates ADR-0002").
- If a change makes a significant decision no ADR covers, propose a new one from
handbook/engineering/decisions/template.mdxrather than losing the rationale in the diff.
/polar-code-review— checks the diff against Polar-specific rules with 2 parallel agents (conventions, ADR compliance). Bugs, security, and simplification are covered by the built-in/code-review,/security-review, and/simplify.
- Handbook: https://handbook.polar.sh/engineering/
- Design docs: https://handbook.polar.sh/engineering/design-documents/
- API guidelines: https://handbook.polar.sh/engineering/rest-api-guidelines
- User/developer docs:
docs/(Mintlify) —cd docs && pnpm devto serve locally.
- Stripe: payments and subscriptions. Needs API keys + webhook secret in
server/.env. - GitHub: authentication and repository features. Needs a GitHub App configured for local dev.
- Slack: workspace integration for notifications. Configured via OAuth at runtime (no
.envsetup). - S3 / Minio: file storage.
- Redis: cache and job queue.
- PostgreSQL: primary database.
.claude/hooks/session-start.sh runs as a SessionStart hook and leaves the container ready to
run tests and linters. It mirrors .github/workflows/test_server.yaml rather than dev up,
because dev up targets interactive local development: it aborts if the Tinybird CLI install
fails, curl-installs nvm, calls systemctl start docker, prompts for GitHub/Stripe, and — worst
for an agent — swallows a failed email-renderer build, the artifact that blocks config import.
It is idempotent, skips work already done, and reports failed steps in its output rather than
aborting. It fires on session start and resume, not on every compaction. Read the script for
what it does; its log is polar-session-start.log in $TMPDIR (/tmp unless overridden).
Deliberately excluded, none of it needed for tests or linters: dev seed, dev start/tmux,
Stripe keys or CLI, GitHub App setup, the Tinybird CLI, dev docker, and the web build. Redis is
replaced by FakeAsyncRedis in tests, Stripe objects are fakes, and Tinybird tests self-skip.
fonts-noto-cjk is also left out — install it only if a PDF or invoice test fails on glyphs.
Two things to know when running tests here:
- An unscoped
pnpm testruns 19 turbo tasks at concurrency 10 on 4 CPUs and produces spuriousTest timed out in 5000msfailures inpackages/checkoutandapps/webthat pass in isolation. Scope with--filter, or pass--concurrency=2. packages/clitests needbun.
Prefer the Polar Development CLI (dev/cli/, alias dev) — the same path local developers use.
See dev/cli/README.md for the full command list. Do not use dev docker (the heavier
image-based stack from the local-environment skill) unless you specifically need it.
Standard lint/test commands live in server/AGENTS.md and clients/AGENTS.md.
Day-to-day start sequence
# Once per VM boot (Docker isn't managed by systemd here):
sudo dockerd > /tmp/dockerd.log 2>&1 &
dev up --skip-integrations # deps, infra (incl. Tinybird), migrations, builds
dev seed # sample orgs/products + [email protected] (NOT part of `dev up`)
dev start # api + worker + web (+ stripe) in tmux session `polar`
# Stop with: dev stop
# Status: dev status--skip-integrations avoids interactive GitHub/Stripe prompts. dev up does not load
sample data; run dev seed afterward. That creates
[email protected] with access to seeded orgs (notably admin-org with a Pro product, plus
acme-corp, polar, etc.). Login OTP codes print in the API pane. If seed says "Already
seeded" (exit 2), the DB already has acme-corp — use dev seed --reset only when you
intentionally want a wipe.
dev start ends by attaching to the polar tmux session; in a non-interactive agent shell,
create/attach then immediately tmux detach-client -s polar, or run dev api / dev worker /
dev web as individual detached processes. The stripe pane of dev start will prompt to
install the Stripe CLI via Homebrew — decline on Linux (no Homebrew); checkout/payment testing
needs a real Stripe sandbox later (dev stripe, see the local-environment skill's
payment-testing rule).
One-time shell wiring (specific to the Cursor Cloud VM snapshot — verify before relying on
any of it, none of it holds in other cloud containers): ./dev/cli/install adds the dev
alias; Node 24 is installed via nvm; uv is at ~/.local/bin/uv. Source ~/.bashrc (or start
a login shell) so nvm use 24 and the dev alias are active. Where the alias is absent, call
./dev/cli/dev directly. clients/ pins Node 24 via .nvmrc/.node-version, but pnpm's
engine-strict is off and the repo ships no .npmrc, so those pins are warnings — install,
lint, typecheck, test and next build all pass on Node 22.
Docker caveats (again Cursor Cloud VM specific). /etc/docker/daemon.json is pinned to
fuse-overlayfs with features.containerd-snapshotter: false — required for Docker 29 in that
VM; don't remove it. The ubuntu user is in the docker group. Elsewhere the file may not
exist and the daemon may run as root on overlayfs; check docker info rather than assuming.
Backend config artifacts. Config import fails without the email renderer binary
(server/emails/bin/react-email-pkg, built by dev up / uv run task emails) and
server/.jwks.json (from ./dev/setup-environment / dev up). Missing → pydantic
EMAIL_RENDERER_BINARY_PATH / JWKS errors. server/.env is not among them for tests:
under POLAR_ENV=testing (which tests/conftest.py forces) polar/config.py loads the
committed server/.env.testing. server/.env is still required before docker compose up -d,
which interpolates it. dev status reports "Worker unknown (check manually)" by design —
confirm with pgrep -af dramatiq or the polar tmux pane.
Tests need no manual DB setup — an autouse sqlalchemy_utils fixture creates and drops
polar_test_<worker_id> (polar_test_master without xdist), not polar_test itself. Run
uv run task test_fast or a subset with POLAR_ENV=testing uv run python -m pytest <path>.
dev/create-test-db creates a plain, unmigrated polar_test and is not part of this path.
Login. Email OTP codes are printed in the API pane / log (LOGIN CODE: …). Grab with
tmux capture-pane -t polar:services.0 -p | grep -a "LOGIN CODE" | tail -1. [email protected]
is the conventional test account.
Onboarding gotcha. The org-creation wizard's "Launch Dashboard" button only submits once the
Product step's required fields are filled (description ≥30 chars, ≥1 selling category, ≥1 pricing
model). The AUP AI check auto-APPROVEs when PYDANTIC_AI_GATEWAY_API_KEY is unset.