Skip to content

Adopt the CodeLit Platform and migrate the workspace to Bun - #81

Merged
s3rot0n1n merged 18 commits into
mainfrom
platform-migration
Oct 19, 2026
Merged

s3rot0n1n merged 18 commits into
mainfrom
platform-migration

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Summary

  • Migrate the monorepo from pnpm, ESLint, and Prettier to Bun and Biome, including CI workflows, Docker builds, and developer documentation.
  • Adopt CodeLit Platform 0.4.1 for shared kernel, observability, billing adapters, and conformance checks.
  • Add organization billing, entitlement infrastructure, and operator recovery workflows; update bootstrap keys and transactional email setup.

Testing

  • Not run (change request content only).

Rajat and others added 18 commits August 30, 2026 02:09
Define OSS and cloud billing modes with provider-backed catalogs,
subscriptions,
checkout, plan changes, webhooks, reconciliation, usage reservations,
fair-use
controls, domain verification, and billing operations tooling. Enforce
organization plan limits across REST, MCP, and workers, update the
Organizations
billing UI and account messaging, and document pricing, errors,
provisioning,
and acceptable-use policies.
Configure runtime telemetry, user identification, session recording, and
logout resets with tests and environment documentation.
app.sendlit.app runs subscriptions-using-billing, so its billing code and
0005_billing migration are the production baseline. main's 0005_equal_patriot
(esp_feedback_connections.subscription_confirmed_at) also shipped, in v0.0.0,
with a later journal timestamp, which would make the Drizzle migrator skip
0005_billing on those databases.

- Keep 0005_billing exactly as production applied it.
- Replace 0005_equal_patriot with 0006_esp_feedback_subscription_confirmed,
  which replays the guarded 0005_billing statements and adds the column with
  IF NOT EXISTS. It is a no-op on whichever half a database already has.
- Re-apply main's new column in schema-core.ts (the branch split schema.ts).
- Use the branch's BOOTSTRAP_* variables and keep main's SUPPRESSION_HASH_KEY.

Verified with PGlite from a fresh database, a production-like database
(0000-0005_billing) and a v0.0.0 database (0000-0005_equal_patriot): all
three converge on the same schema.
Follow the CodeLit Platform toolchain: one Bun 1.4.1 workspace and bun.lock,
the API running its TypeScript directly on Bun, and Bun-based images and CI.

- Root package.json carries workspaces, trustedDependencies and overrides;
  pnpm-workspace.yaml and pnpm-lock.yaml are gone.
- apps/api runs with `bun src/index.ts` (no tsc emit); tsconfig uses bundler
  resolution with noEmit and covers tests, so tsconfig.test.json is gone. Bun
  loads .env itself, so the dotenv calls are removed. The Compose init service
  and health check use bun.
- @sendlit/api-contract exports its TypeScript source like the Platform
  template's contract package, so nothing has to build it first. The web and
  docs apps transpile it; email-blocks imports its one type from a schema
  subpath so the bundled types stay portable.
- The API image builds email-editor/email-blocks, then runs on oven/bun with a
  production install. The web image builds with Bun and runs the standalone
  server on Node.
- CI uses oven-sh/setup-bun with frozen installs and now typechecks.
  Dependabot groups @codelitdev/* updates.
- ci:publish resolves workspace: ranges before changeset publish, as MediaLit
  does.
- Bun resolved ranges afresh, so pin what broke: next 16.3.6, react 19.2.8
  (the preset versions), lucide-react ^0.577 to share fumadocs' copy,
  mdast-util-to-markdown 2.1.2 (2.2.0 recurses in fumadocs' stringifier) and
  @types/express 4.17.25.
- TypeScript 5.9.3 everywhere.
- apps/docs/.source is generated on install; stop tracking it.
Lint and format with Biome 2.5.12 using the CodeLit Platform template config,
plus SendLit ignores (generated docs source, drizzle SQL, managed Platform
files, SVG assets) and an override for the editor packages' Tailwind 3 CSS
directives.

- Root scripts: lint (biome lint .), format, format:check. The pre-commit hook
  runs `bun run lint`, as in Platform; CI runs lint and format:check.
- Remove eslint.config.cjs, the editor packages' .eslintrc.js, .prettierrc,
  .prettierignore, lint-staged and their dependencies.
- Fix what the recommended rules report without changing behavior: import
  type, node: protocol, explicit let types, a useless rethrow, unused
  variables, Number.isNaN on parsed numbers, button types on editor buttons,
  stable keys where the data has one. Remove two dialogs nothing rendered
  (EspFormDialog, OrganizationEspFormDialog).
- Convert eslint-disable comments for react-hooks/exhaustive-deps into
  biome-ignore suppressions with the reason. Other suppressions keep shadcn
  markup, <img> for arbitrary MediaLit hosts, and document.cookie as is.

The repository-wide reformat follows in a separate commit.
Run `biome check --write --linter-enabled=false .`: Biome formatting
(two-space indent, 88 columns) and import sorting. No other changes.
The CodeLit Platform runs tests with `bun test`. Every workspace now uses
`bun test --parallel`, which isolates each file (fresh globals and module
registry) the way Vitest did. Test counts are unchanged in every file:
api 567, web 68, sendlit 68, email-editor 12, email-blocks 8.

- Test files import from bun:test. Bun does not hoist module mocks, so each
  file registers mock.module() first and loads the code under test with
  `await import()`, keeping import-time side effects (db client, Redis,
  BullMQ) out of tests as before.
- New private @sendlit/test-utils fills the gaps in Bun's Vitest API:
  stubEnv/stubGlobal and their resets, waitFor, resetModules, and a
  happy-dom registerDom() for the React component tests.
- apps/api: bunfig.toml preloads src/test/setup.ts; PGlite migrations resolve
  from import.meta.dir.
- getBillingEngine() skipped its cache only under Vitest's VITEST variable;
  check NODE_ENV=test, which bun test sets, instead.
- tsup builds of the editor packages no longer pick up test files.
- Remove Vitest, its configs, and the unused jsdom dependency in apps/web.
Every @codelitdev package moves to 0.4.1, which all Platform packages now
share. External dependencies use the exact versions in platform-cli's preset,
so `platform-cli doctor` reports no issues: better-auth and its OAuth
provider 1.7.7, express 4.21.2, pg 8.23.0, pino 9.6.0, drizzle-orm 0.45.2,
drizzle-kit 0.31.10, PGlite 0.5.8, ts-rest 3.52.1, zod 3.25.76 and
dodopayments 2.48.0. apps/api also depends on @codelitdev/platform,
observability and mcp-server-kit, and on platform-conformance for tests.

Better Auth 1.7.7 checks the database schema at runtime:
- 0007_better_auth_1_7_7 makes account.issuer nullable (1.7.7 no longer
  writes it; drop it in a later release) and adds the
  oauth_access_token.revoked column the OAuth provider has always declared.
- oauth-server-kit 0.4.1 allows registering every valid audience; keep
  dynamic registration limited to the MCP resource SendLit seeds.

billing.generated.ts is regenerated by codelit-billing 0.4.1. The schema is
unchanged from alpha.3; only the generator's formatting differs.
The API now composes the published Platform packages instead of SendLit's
copies. This commit builds together with the next one, which moves billing
onto the same packages (index.ts already wires its lifecycle).

Kernel (@codelitdev/platform):
- The Express app moves to createApp() (src/app.ts); index.ts starts workers
  and loops, serves /ready from readinessReport (startup, shutdown, Postgres,
  Redis), and on SIGTERM runs one ordered graceful shutdown: stop HTTP, let
  BullMQ workers finish their job, close queues, Postgres and telemetry.
  /health uses healthReport; every response carries X-Request-ID.
- Requests accept exactly one credential (Platform ADR 0001), using the
  kernel's extraction plus x-sendlit-apikey and the legacy body key; X-API-Key
  now works too. Several credentials return 400 credential_ambiguous, a
  malformed Authorization header is rejected, and nothing falls back. MCP does
  not treat a browser session as a credential. OAuth tokens are verified by
  oauth-server-kit's verifyOAuthAccessToken; the team_id claim is read from
  the verified token and membership is still re-checked.

Observability (@codelitdev/observability):
- One instance carries pino logs, error capture and OTLP log shipping to
  PostHog, as in MediaLit. Product events use a second instance with dedupe
  and per-source caps off, since the package would otherwise drop all but the
  first email_sent per team per minute. posthog-node, pino-opentelemetry-
  transport and POSTHOG_LOG_LEVEL are gone; unhandled rejections are captured.
- The web app's browser PostHog uses createBrowserObservability.

Conformance:
- .codelit-platform.json and the managed platform-conformance workflow come
  from `[email protected] sync`; doctor reports no issues.
- test:conformance runs Platform's MCP discovery suite against the real app,
  SendLit's kernel-rule checks, and the MCP/REST parity manifest
  (packages/api-contract/src/mcp-parity.ts, validated with mcp-server-kit).
- check:drift runs codelit-billing generate --check and compares Better
  Auth's expected schema with the Drizzle tables.
- MCP keeps SendLit's SDK 2 stateless server; mcp-server-kit's transport and
  CORS are built on SDK 1 sessions (see docs/decisions/0001).

OpenAPI generation moves into @sendlit/api-contract/openapi so the API and the
docs site render one document; it documents X-API-Key and the
credential_ambiguous response. AGENTS.md, the README, the API README and the
developer docs describe the Platform conventions, and ADR 0001 records the
decisions.
…adapters

app.sendlit.app runs this billing code; it moves from billing 0.1.0-alpha.3
to 0.4.1 and the @codelitdev/platform/billing adapters (ADR 0013), following
MediaLit's adoption. The generated schema is unchanged.

- Action grants come from createBillingActionGrants with the Better Auth
  verification store. A second grants instance without the recent-auth limit
  keeps resuming checkout for a pending_payment organization working. Engine
  action tokens are bound to the organization and its owner when issued.
- Sensitive values use the shared AES-GCM port (same key, previous-key and
  ciphertext format, so stored values stay readable); return URLs use
  createReturnUrlValidator.
- Paid access comes from commercialState(), inside the caller's transaction
  when reserving quota (subscriptions are locked first). SendLit's own
  reconciliation sweeps, expiry checks, second webhook projector and the
  plan_states.plan copy are removed; the column stays until a later release.
- Webhooks go through handleBillingWebhook at /webhooks/billing/:provider
  (Dodo's URL is unchanged). Storage failures now return 503 so Dodo retries.
- startBillingMaintenance runs startBillingWorkers plus SendLit's own loops
  and stops cleanly on shutdown. Worker IDs include the hostname.
- product-effects never reactivates an abandoned, closed or suspended
  organization on a late payment, and releases the trial claim when a
  checkout is abandoned.
- Cancellation now keeps access until the end of the paid period. New
  POST .../billing/cancel and .../billing/resume routes, paidThroughAt in the
  billing response and get_plan_usage, and Resume / Cancel plan in the
  dashboard.
- Optional DODO_BRAND_ID for a shared Dodo business.
- The operator CLI uses createOperations and adds health, inspect-org and
  reconcile-cancellations. Run reconcile-cancellations against production
  before switching traffic to this release (apps/api/README.md).
Bun's filtered installs need the manifest of every workspace a package
depends on, including the dev-only @sendlit/test-utils. The web image also
type-checks test files during next build, so it gets the test-utils source
and apps/web declares @types/bun itself.
- Read allowed origins from `DEV_ALLOWED_ORIGINS`
- Add organization plan policy and trial claim tables with data migration
- Remove SendLit-specific columns from canonical billing schema
@s3rot0n1n
s3rot0n1n merged commit 2e6f56a into main Oct 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants