Skip to content
DocsmigratingDocumentation

Upgrading from alpha to 1.0

How a 1.0.0-alpha.* app moves to stable — dependency bumps plus every breaking change already landed on the road to 1.0.

Last updated:

This guide collects everything a 1.0.0-alpha.* app needs to reach stable 1.0.0: the dependency bump strategy, and the breaking changes that have already landed on the alpha channel.

Stable 1.0.0 has not shipped yet. This page is a living document: each breaking change that lands on the alpha channel before the stable cut is recorded here, so it will be updated as 1.0 approaches. If you track the alpha dist-tag, treat this page as your changelog digest.

Bump strategy

Projects scaffolded by lunora init pin concrete alpha versions of lunorash and any @lunora/* add-ons (the scaffolder resolves the CLI's release-channel dist-tag to an exact version at init time). When 1.0 ships:

  1. Move every Lunora dependency at once. The packages are released as a coordinated train; mixing an alpha runtime with a stable client (or vice versa) is unsupported. Bump lunorash and each @lunora/* add-on to the latest dist-tag together:

    pnpm add lunorash@latest
    pnpm add @lunora/vite@latest @lunora/auth@latest   # …and every other @lunora/* you use
  2. Regenerate. _generated/ output is tied to the codegen version:

    pnpm lunora codegen
  3. Verify before deploying. lunora verify validates wrangler.jsonc, dry-runs codegen, and type-checks without writing files:

    pnpm lunora verify
  4. Read the sections below and fix any usage of a removed API. TypeScript catches all of the removals at compile time.

Packages in the experimental tier (see Versioning & stability) stay on prerelease versions after 1.0; keep those pinned to their own channel.

Breaking changes already landed

Each of these shipped on the alpha channel as a BREAKING CHANGE (or a deliberate surface cut) on the road to 1.0. If your app started on an early alpha, work through them in order.

@lunora/ratelimit: shards option removed

RateLimitConfig.shards split a limit into N sub-buckets that each held rate / shards, and routed every key to exactly one of them, so a limit enforced rate / shards rather than rate (a rate: 10_000, shards: 16 limit admitted 625 per period). Every sub-bucket lived in the same store, so the split relieved no contention either.

Migration: delete shards from every limit config. The limit then enforces the rate it declares, which is what useRateLimit already predicted. If you tuned rate upward to compensate for the split, set it back to the real limit — a rate raised 16× for a 16-way split now admits 16× the traffic. A config that still carries shards throws at new RateLimiter(...), so a missed one fails at startup instead of silently enforcing a different limit.

Stored state does not carry over. A sharded limit stored each key's state under <name>:<key>#<shard>; the unsharded limit reads <name>:<key>. After the upgrade:

  • every previously sharded key starts fresh, so a caller at its limit gets a new budget once, and any tokenBudget debt recorded against it is forgiven;
  • the old #<shard> rows are never read again, and reset() does not remove them. With createMemoryStore they vanish on the next restart; with createSqlStore / createDbStore they stay until you delete them, for example every key ending in # followed by digits in the limiter's table.
// Before
ingest: { kind: "token bucket", period: 1_000, rate: 10_000, shards: 16 },

// After
ingest: { kind: "token bucket", period: 1_000, rate: 10_000 },

@lunora/ratelimit: random option removed

The deprecated RateLimiterOptions.random shard-selection hook is gone. It was already unused when it was removed, and shards itself is now gone too (see above).

Migration: remove any random property from new RateLimiter({ ... }) calls. No behavioral replacement is needed.

// Before
const limiter = new RateLimiter({ config, random: Math.random });

// After
const limiter = new RateLimiter({ config });

@lunora/runtime: authIntrospector removed

The deprecated WorkerOptions.authIntrospector read-only fallback and the exported AuthIntrospector type alias are removed. Wire WorkerOptions.authAdmin instead, e.g. @lunora/auth's createAuthAdmin(auth).

A browse-only plane still works: listUsers and listSessions are the only required AuthAdmin members, so passing the same read-only object as authAdmin keeps the Studio's browse endpoints working while the mutation routes respond AUTH_OP_NOT_SUPPORTED.

// Before
export default createWorker({ authIntrospector: myIntrospector /* … */ });

// After
import { createAuthAdmin } from "@lunora/auth";

export default createWorker({ authAdmin: createAuthAdmin(auth) /* … */ });

@lunora/server: mode: "incremental" cut from external sources

mode: "incremental" on .source(...) was accepted by the types but threw "not yet implemented" at defineSchema time. It is cut from the 1.0 surface: ExternalSourceMode is narrowed to the single literal "full-pull", and the incremental-only reconcileEveryMs knob is dropped. A stray mode is now a compile-time error instead of a runtime throw.

Migration: if you declared mode: "incremental" (it never worked), switch to mode: "full-pull" (or omit mode entirely) and remove any reconcileEveryMs. Incremental pulls may return after 1.0 as a new feature.

@lunora/cli: migrate create requires a table

lunora migrate create <name> no longer writes a literal TODO_table placeholder when --table is omitted. Interactively it prompts for the target table (offering the tables declared in lunora/schema.ts); non-interactively (CI, scripts) it fails with a clear error.

Migration: pass --table explicitly in any scripted invocation:

lunora migrate create backfill_read_by --table messages
  • Enforce ephemeral WS admin tokens. The worker can now reject the raw master LUNORA_ADMIN_TOKEN in WebSocket ?token= query strings in favor of short-lived minted sub-tokens. Off by default; see the production checklist for how to turn it on.

See also