Engineering

Changelog

A systems diary: ownership boundaries, enforcement seams, and infrastructure contracts as they land. Each entry records an engineering decision — not a feature announcement.

September 2026

Canonicalized the public blog index to /blog/

Google should see one hub URL. Trailing slash is canonical. /blog 301s there.

  • Nav, sitemap, RSS, and llms.txt emit /blog/ from one shared BLOG_INDEX_PATH constant.
  • /blog 301s to /blog/ so crawlers do not split equity across two hub URLs.

Documentation →

September 11, 2026

  1. Matched local-verification to the CI codegen and production smoke gates

    The laptop gate should fail the same way CI fails. Generated GraphQL and the production static container sit on the same checklist.

    • make local-verification now fails on uncommitted GraphQL codegen the same way CI codegen-verification does.
    • The last step builds Dockerfile.production and smokes marketing routes, including the /blog to /blog/ 301.

    Documentation →

August 2026

Shipped Platform MCP with OAuth isolated from portal GraphQL Bearer

Cursor should not inherit a portal session. MCP credentials mint with audience=mcp. One kill switch. Exact redirect URIs per environment.

  • OAuth 2.1 with PKCE and DCR issues audience=mcp credentials. Super-admin only. Portal Bearer stays a different audience.
  • Streamable HTTP JSON-RPC lives at POST /mcp with a scope-gated mcp_whoami smoke tool.
  • mcp.enabled plus per-env allowList own redirect policy. Loopback is always allowed. Non-loopback hosts need an exact URI. Wildcards are forbidden.

Documentation →

August 20, 2026

  1. Moved portal auth onto dual opaque credentials and httpOnly cookies

    GraphQL identity no longer lives in localStorage. Access is short-lived. Refresh rotates the pair. Super admins can revoke a session family without a global logout.

    • Portal delivers auth_access and auth_refresh as httpOnly cookies. Opaque clients still send Authorization: Bearer. Bearer wins when both are present.
    • Refresh consumes the refresh row atomically and requires the paired access token from the same session_family_id. Redis is the hot path. Postgres revoked_at is the source of truth.
    • signOut revokes this device's session family. Super-admin session governance at /admin/super/sessions covers inspect, revoke-one, revoke-user, revoke-org, and global flush.

    Documentation →

August 5, 2026

  1. Added previous_unit_amount and most_popular, and locked unit_amount to cents everywhere

    Strikethrough pricing and a popular-plan badge, both driven from the super admin panel. Plus a cleanup: unit_amount is cents end to end now, converted only at render time.

    • previous_unit_amount on the price table powers strikethrough display when you drop a price without touching the checkout flow.
    • most_popular on the product table flags a plan for the badge, enforced by a partial unique index so only one plan can hold it.
    • unit_amount now stays in cents through the entire pipeline — conversion to display currency happens only in frontend and email templates, fixing spots where it wasn't.

July 2026

Shipped provider-agnostic feature flags behind tools.featureFlags

Rollouts and experiments should swap vendors without rewriting product code. One contract. PostHog, LaunchDarkly, or a local stub.

  • tools.featureFlags evaluates flags with request-level and optional shared Redis caching so GraphQL reads stay consistent.
  • PostHog and LaunchDarkly adapters sit behind the same interface; LOCAL covers development without an external service.
  • me.featureFlags exposes only keys you whitelist in config — operational flags stay off the public surface.

Documentation →

July 31, 2026

  1. Hardened verification and cleaned up catalog and editor UX

    Local gates should match how CI and cloud agents actually run, and admin surfaces should not fight the operator.

    • make local-verification supports compose and native backends so Docker and host-side checks share one contract.
    • Product create no longer offers a misleading currency selector when currency comes from the price model.
    • Blog composer bubble menu only appears with a real text selection; Delete actions keep readable contrast in dark mode.

    Documentation →

July 28, 2026

  1. Made pricing predictable for both builders and customers

    Pricing should have one source of truth, and customers should pay the price they agreed to when they signed up.

    • One public products query powers pricing pages, with support for recurring and one-off plans.
    • Trials retain a fallback price so grandfathered customers can seamlessly convert to paid plans.
    • Billing documentation now reflects the platform's product and price model, keeping the catalog story consistent.

    Documentation →

  2. Made production, documentation, and public surfaces preserve intent end-to-end

    What you write, install, and ship should behave consistently across development, production, and every surface your users see.

    • Production builds automatically include tooling packages the same way local installs do, with CI validating that contract.
    • Operational scripts now share a consistent "how to run" experience locally and in production, with clearer help text.
    • Markdown tables round-trip correctly through the admin editor and render as proper HTML tables on public articles.
    • Link previews and social scrapers now display titles correctly, including apostrophes and other escaped characters.
    • Getting Started has been simplified to Quickstart plus What you get, with deeper setup moved under Architecture and Support.

    Documentation →

July 22, 2026

  1. Improved blog discovery so published posts show up where search and feeds look

    When an article goes public, sitemap, RSS, and llms.txt update with it. No more waiting on a delayed job for indexes to catch up.

    • Discovery regenerates on real visibility changes, including past-due scheduled posts and CMS publish or unpublish edits.
    • Daily and weekly reconcile also refresh discovery so CDN indexes stay aligned even if the queue runs late.
  2. Made local setup faster with clearer env files and one-shot Stripe wiring

    Getting Refract running locally should not mean wrestling secrets or guessing which env file matters.

    • Environment-specific env files only. No generic .env required to boot.
    • make stripe-setup authenticates the Stripe CLI and writes the webhook secret in one pass.
    • Mailer from-address defaults ship in .env.example so email config stops blocking first boot.

July 20, 2026

  1. Upgraded the billing data model: Product and Price are separate tables

    You can change pricing without rewriting the product. Products own the catalog identity; prices own amount, currency, interval, and the processor billable id.

    • Retire a price and keep existing customers on their terms via optional price pins on subscriptions and checkout.
    • Plan-change previews and usage-based plans sit on the same model, so catalog work stays one workflow.
    • A migrate script moves legacy catalog rows onto the new shape with journal and revert when you need a safe upgrade path.

    Documentation →

July 7, 2026

  1. Marketing SSR guardrails after production postmortem

    The embedded SSR incident showed CI never exercised the single-container path. Phase 1 locks the wiring in config validation and tests so the same class of drift cannot ship quietly again.

    • Config contract tests assert production, staging, and development wire dist/client, entry.mjs, and loopback GraphQL together — no backend:3000 in embedded paths.
    • Zod validation requires ssrEntryPath and graphqlUrl as a pair; static-only marketing config remains valid for test environments.
    • browser-static integration test loads a real temp .mjs through defaultMarketingSsrModuleLoader and serves /blog — guards the ts-node import regression.
    • CI integration workflow runs full marketing pnpm test (astro check + vitest), not lint alone.
  2. Embedded marketing SSR fixed for production; local dev hardened

    Caught dogfooding userefract.io.

    • Marketing serves prerendered pages from dist/client before falling back to Astro SSR, with GraphQL wired over loopback.
    • Fixed SSR resolution under ts-node.
    • Image URLs tightened to same-origin; JSON-LD only uses absolute URLs.
    • Pricing catalog resilient to an empty state at build time.

July 3, 2026

  1. Blog CMS: Postgres source of truth, CDN MDX, and public GraphQL

    Most boilerplates leave content to you. Refract ships a production blog on day one, so distribution isn't a separate project you bolt on later.

    • Super-admin portal manages articles, authors, pillars, series, and media. Publish writes MDX to CDN and sets public GraphQL fields — resolvers stay thin, utilities own side effects.
    • Scheduled posts reconcile via scheduler plus blogArticlePublish queue consumer; rate-limited publicBlog queries feed SSR marketing routes at /blog/article/{slug}.
    • Legacy Astro content-collection blog removed; sitemap, changelog links, and redirects target the new article paths.

    Documentation →

June 2026

Pluggable scheduler: recurring jobs as first-class config

  • Define a schedule name, cron pattern, and timezone — the framework handles the rest. No scattered setInterval, no queue hacks to fake recurrence.
  • Provider-swappable from day one: BullMQ job schedulers in production, a zero-infrastructure client for Jest. Same handler code either way.

Design breakdown →5 min read

June 23, 2026

  1. Local dev proxy:: Streamline dev-proxy routing

    • Fixed local dev routing on :8888 where visiting portal pages (e.g. /signin) could break marketing pages like /pricing with wrong Vite assets, 404s, or invalid React hook errors.
    • Changed dev proxy to a simple path-based ladder: portal dev assets under /__portal/, marketing keeps root /@vite and /src; no more dev_vite_upstream cookies or marketing path manifest maintenance for new Astro pages.
  2. Marketing: config-driven landing and live catalog pricing

    • landing.config.ts owns theme, sections, SEO, navigation, and legal placeholders — composable section library with light, dark, and system theming.
    • publicPlans GraphQL feeds /pricing at build time. Product descriptions drive cards; default plans stay hidden; CTAs route signup to billing checkout.

    Documentation →

June 22, 2026

  1. Local dev: deps-install and consumer runtime sync

    • Consumer installs dependencies on boot and waits for backend health — git pull cannot leave tooling packages missing on a stale node_modules volume.
    • make deps-install syncs host and runtime deps in one pass; make deps-fix rebuilds from a clean volume when the lockfile or symlinks drift.

June 16, 2026

  1. CDN: pluggable uploads behind tools.cdn

    • Optional tools.cdn loads from config — local noop for dev and test, Cloudflare R2 for production. Omit the tool and upload mutations fail gracefully instead of crashing.
    • Profile picture upload runs GraphQL multipart to object storage and surfaces a public CDN URL on the admin profile page.

June 12, 2026

  1. Profile: capability-driven auth and identity

    • /admin/profile covers personal info, email change, password management, and connected accounts. UI branches on deriveAuthCapabilities — not provider-specific conditionals.
    • Pending email verification, OAuth connect and disconnect with last-method guards, and centralized OAUTH_PROVIDERS keep sign-in, sign-up, and profile on one contract.

June 9, 2026

  1. Scheduler: stable job IDs via Job Schedulers API

    • Production reconcile migrated to BullMQ's Job Schedulers API — one stable id per schedule, no duplicate jobs on redeploy.
    • Orphan cleanup removes legacy repeatable keys automatically. Pattern or timezone changes upsert in place, no manual Redis surgery.

    Design breakdown →5 min read

  2. Analytics: ANALYTICS_APP required per deployment

    • Every outbound event is now stamped with an app identifier — multiple Refract deployments stay separable inside a single analytics project.
    • Missing config fails at startup, not silently at the first track call.
  3. Marketing: Google Consent Mode v2

    • Analytics and ads consent default to denied. Grants flow through the shared portal cookie contract — no custom wiring needed.
    • gtag consent updates stay in sync with googleAnalyticsAllowed, closing the race window between banner render and first pageview.

May 2026

Phased trials: subscription-schedule orchestration with durable ownership

  • Trial boundaries live on Stripe subscription schedules. Phase composition and schedule writes stay in the processor, not handlers.
  • Re-trial and trial/downgrade handoffs reconcile out-of-order webhooks. Idempotency owns side effects at the seam.

May 29, 2026

  1. Scoped product limits: policy, metering, and one enforcement seam

    • Quota policy attaches to products: billing-cycle or all-time metering, org pool or per-membership allowance. Unscoped roles bypass; scoped roles resolve to limited or blocked.
    • One wrapper owns enforcement and accounting — policy and cycle resets never leak into handlers.

    Systems note →5 min read

May 25, 2026

  1. Provider-agnostic AI: one tools.ai containment contract

    • Anthropic, OpenAI, and Google route from config. Model swaps do not require SDK imports or handler rewrites.
    • Complete, stream, and token-count paths share one contract — retries and instrumentation stay behind tools.ai.

    Design breakdown →5 min read

May 22, 2026

  1. Customer profile sync boundary between portal and Stripe

    • Profile edits in admin and checkout write through one customer seam — no Stripe Dashboard detour.
    • Validation and error copy live in the tools layer so portal display and processor sync cannot drift.

May 17, 2026

  1. One-off checkout on the subscription billing contract

    • Add-ons and single charges reuse subscription checkout — one payment contract, no parallel stack.
    • Local billing stack boots reliably so the full money path is exercisable without production shortcuts.

May 12, 2026

  1. Local dev proxy: single-origin routing contract for the full stack

    • One origin routes cookies, OAuth, and HMR through production-shaped matchers across marketing, portal, and API.
    • Vite asset isolation prevents portal and marketing dev bundles from cross-routing under one hostname.

April 2026

Invoice lifecycle control from webhooks to portal

  • Invoice records own create, update, and paid paths — portal history tracks Stripe documents.
  • Dedicated webhook ownership closes reconciliation gaps when events arrive out of order.

April 26, 2026

  1. Upgrade checkout: customer-field enforcement before payment

    • Tax and customer fields validate before payment intent — failures surface at the boundary, not after capture.
    • Saved profiles sync through the same customer seam as admin edits.

April 23, 2026

  1. Password policy contract shared by UI and API

    • Sign-up, reset, and profile flows call the same validators as the API — no UI-only rules.
    • Auth seam stays aligned: allowed passwords cannot differ between client and server.
  2. Express mount contract for routers and auth

    • New routes wire through one mount pattern — fewer divergent auth and handler stacks.
    • Deployment surface trimmed to modules the monorepo actually runs.