v2.0.0 — your ORM, Redis, API keys, observability and browser tests
After usage-based billing, the roadmap had six milestones left. They are all in v2.0.0. It is a major version because the kit now needs Redis, and because transactions work differently: both are covered under Upgrading below.
Your choice of ORM
Pick Drizzle, Prisma or TypeORM with one setting, DATABASE_ORM, when you set the product up, and write your features with it. The kit’s own modules work with all three:
- Repositories: each module that stores something reads and writes through a small repository, with one implementation per ORM.
- Transactions that join:
database.transaction(async () => …)keeps the open transaction inAsyncLocalStorage. Anything called inside it takes part: the audit entry, the queued email, the usage record. Nothing passes atxaround any more. - Migrations: each ORM writes and runs its own. In production they run without the ORM’s CLI in the image.
- Tested on all three: a test builds the database with each ORM and checks they match column for column and index for index. Every API test then runs again on Prisma and on TypeORM in CI.
Redis
- The job queue runs on BullMQ. Jobs are still written to Postgres first, in the transaction of the change they belong to, so a job exists only if its change was saved. A relay hands them to BullMQ once the transaction commits.
defineJob,@OnJoband@Scheduledare unchanged. - Realtime: a WebSocket for each open workspace, through a Redis stream, so an event reaches everyone however many copies of the API run. Usage and workspace changes update other people’s screens as they happen.
- Assistant answers survive a closed tab. Answers are kept in Redis while they are written, and the chat picks them up where they were.
- Shared rate limits for sign-in, API keys and your own features, and a cache for your features whose keys are always scoped to a workspace.
API keys
Programs call your product with Authorization: Bearer sk_….
- Owned by the workspace: a key belongs to the workspace, not to whoever made it, and has the admin or member role.
- The same rules as a person: it reaches the same routes, under the same role and plan checks.
- Shown once: it is stored as a hash. It can be given an expiry, and is limited to 120 requests a minute.
- Billable: API calls can be charged as usage on any plan.
- Settings page: admins create and revoke keys there, and both are recorded in the audit log.
Seeing what production does
- Traces and metrics: set
OTEL_EXPORTER_OTLP_ENDPOINTand the API sends OpenTelemetry traces to any backend. A request’s trace includes its queries, whichever ORM sent them, and the jobs it queued, however much later they ran. Metrics cover request latency, job duration and queue depth. - Error reports: set
SENTRY_DSNand errors are reported to Sentry from the API, the web app and the mobile app. They are filed under the release, with source maps, and never carry a password, token, cookie or email address. - Product analytics: set
MIXPANEL_TOKEN. Sign-ups, invitations and every change of plan are sent from the API, and page and screen views from the apps. Events are named once, in the contracts.
Every journey, in a real browser
packages/acceptance runs Playwright against the production images, the same ones you deploy, Caddy included. Stripe’s checkout and the AI model are replaced by stand-ins, and emails are read from a local inbox.
What the journeys walk through:
- Accounts: signing up and verifying, passwords and sign-in links, changing an email address in both inboxes, devices, deleting an account.
- Teams: invitations, roles, seat limits, the switcher, workspace settings.
- Billing: paying and the receipt, cancelling.
- The assistant: answering, Stop, and an answer picked up after a reload.
- The rest: API keys, the audit log, the admin panel including signing in as a customer and suspending someone, and live updates between two browsers.
They run in personal and teams mode, on each ORM, and with Polar, on every pull request. bun run acceptance runs them on your machine.
The mobile app has flows of its own, with Maestro, against the same images: signing up, the account screens, the assistant, and accepting an invitation with teams on. bun run acceptance:mobile runs them on the iOS simulator or an Android emulator. CI runs Android every night and whenever the app changes. Writing them found real bugs, which are fixed in this release. One example: a workspace renamed on one device did not show on another until the page changed.
Upgrading
v2.0.0 needs Redis.
- Run
bun run services:uplocally. On Railway,bun run deploy:applyadds the Redis service. - In your own services, inject your ORM’s database class (
DrizzleDatabase) in place ofDB, and query through itsclient. - Replace any
txoption, anddb.transaction(async (tx) => …), withdatabase.transaction(async () => …). - Run
bun run db:migrate. It adds the API keys table, and drops the columns from before Polar that v1.2.0 stopped writing, as its notes said it would.
You are on Drizzle unless you set DATABASE_ORM, so nothing else changes. Jobs waiting in the old queue move across by themselves. UPGRADING.md has the full list, and the upgrade-kit skill applies it on a branch.
Next
Notifications for one person, from any module with one call:
- Visible ones: stored, shown with a bell and an unread count, and also sent to their phone or by email, as they choose.
- System events: sent to their open apps only, for “look again” or “your export is ready”.
The roadmap has the details.
Get the next one by email
Only project updates. Unsubscribe with one click.