@lunora/cli 0.0.0 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +109 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/bin.mjs +11 -0
  5. package/dist/index.d.mts +852 -0
  6. package/dist/index.d.ts +852 -0
  7. package/dist/index.mjs +19 -0
  8. package/dist/packem_chunks/handler.mjs +76 -0
  9. package/dist/packem_chunks/handler10.mjs +22 -0
  10. package/dist/packem_chunks/handler11.mjs +192 -0
  11. package/dist/packem_chunks/handler12.mjs +131 -0
  12. package/dist/packem_chunks/handler13.mjs +65 -0
  13. package/dist/packem_chunks/handler14.mjs +58 -0
  14. package/dist/packem_chunks/handler15.mjs +79 -0
  15. package/dist/packem_chunks/handler16.mjs +41 -0
  16. package/dist/packem_chunks/handler17.mjs +105 -0
  17. package/dist/packem_chunks/handler18.mjs +172 -0
  18. package/dist/packem_chunks/handler19.mjs +89 -0
  19. package/dist/packem_chunks/handler2.mjs +114 -0
  20. package/dist/packem_chunks/handler20.mjs +94 -0
  21. package/dist/packem_chunks/handler21.mjs +311 -0
  22. package/dist/packem_chunks/handler3.mjs +204 -0
  23. package/dist/packem_chunks/handler4.mjs +33 -0
  24. package/dist/packem_chunks/handler5.mjs +49 -0
  25. package/dist/packem_chunks/handler6.mjs +91 -0
  26. package/dist/packem_chunks/handler7.mjs +42 -0
  27. package/dist/packem_chunks/handler8.mjs +174 -0
  28. package/dist/packem_chunks/handler9.mjs +16 -0
  29. package/dist/packem_chunks/planDevCommand.mjs +543 -0
  30. package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
  31. package/dist/packem_chunks/runDeployCommand.mjs +504 -0
  32. package/dist/packem_chunks/runInitCommand.mjs +652 -0
  33. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +397 -0
  34. package/dist/packem_chunks/runResetCommand.mjs +41 -0
  35. package/dist/packem_chunks/runRpcCommand.mjs +68 -0
  36. package/dist/packem_shared/COMMANDS-1V_KEx35.mjs +905 -0
  37. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +244 -0
  38. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +19 -0
  39. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +13 -0
  40. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +38 -0
  41. package/dist/packem_shared/command-BDXcJCCJ.mjs +14 -0
  42. package/dist/packem_shared/createLogger-CHPNjFw2.mjs +73 -0
  43. package/dist/packem_shared/defaultSpawner-DxI3mebw.mjs +43 -0
  44. package/dist/packem_shared/diffSnapshots-RR2ZE8Ya.mjs +161 -0
  45. package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -0
  46. package/dist/packem_shared/features-ocSSpZtS.mjs +24 -0
  47. package/dist/packem_shared/insertSchemaExtension-BuzF6-t2.mjs +59 -0
  48. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +41 -0
  49. package/dist/packem_shared/output-format-7gyGR3h8.mjs +17 -0
  50. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +56 -0
  51. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +94 -0
  52. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +16 -0
  53. package/dist/packem_shared/runAddCommand-BZGkRnBs.mjs +693 -0
  54. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
  55. package/dist/packem_shared/schemaIrToSnapshot-aBTo7TM5.mjs +43 -0
  56. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
  57. package/package.json +61 -18
  58. package/skills/README.md +29 -0
  59. package/skills/lunora/SKILL.md +83 -0
  60. package/skills/lunora-create-package/SKILL.md +129 -0
  61. package/skills/lunora-deploy/SKILL.md +150 -0
  62. package/skills/lunora-functions/SKILL.md +182 -0
  63. package/skills/lunora-migration-helper/SKILL.md +194 -0
  64. package/skills/lunora-performance-audit/SKILL.md +143 -0
  65. package/skills/lunora-quickstart/SKILL.md +240 -0
  66. package/skills/lunora-realtime/SKILL.md +177 -0
  67. package/skills/lunora-setup-auth/SKILL.md +170 -0
  68. package/skills/lunora-setup-hyperdrive/SKILL.md +154 -0
  69. package/skills/lunora-setup-hyperdrive-global/SKILL.md +171 -0
  70. package/skills/lunora-setup-mail/SKILL.md +151 -0
  71. package/skills/lunora-setup-scheduler/SKILL.md +157 -0
  72. package/skills/lunora-setup-storage/SKILL.md +154 -0
@@ -0,0 +1,240 @@
1
+ ---
2
+ name: lunora-quickstart
3
+ description: Creates or adds Lunora to an app. Use for new Lunora projects, `lunora init`,
4
+ framework/provider wiring, the first `lunora dev` run, env vars, or writing
5
+ the first schema + query/mutation round-trip.
6
+ ---
7
+
8
+ # Lunora Quickstart
9
+
10
+ Set up a working Lunora project as fast as possible.
11
+
12
+ ## When to Use
13
+
14
+ - Starting a brand new project with Lunora.
15
+ - Adding Lunora to an existing Vite, Next.js, Astro, Nuxt, SvelteKit, or
16
+ TanStack Start app.
17
+ - Scaffolding a Lunora app for prototyping.
18
+
19
+ ## When Not to Use
20
+
21
+ - The project already has Lunora installed and `lunora/` exists — just build,
22
+ and run `lunora codegen` after schema/function edits.
23
+ - You only need to add auth to an existing Lunora app — use the
24
+ `lunora-setup-auth` skill.
25
+
26
+ ## Workflow
27
+
28
+ 1. Determine the starting point: new project or existing app.
29
+ 2. New project: scaffold with `lunora init` and pick a template.
30
+ 3. Existing app: run `lunora init --here` to patch the Vite config and wire
31
+ Lunora into the current project.
32
+ 4. Run `lunora codegen` to generate `lunora/_generated/` and typecheck the
33
+ schema + functions. This is the agent's feedback loop.
34
+ 5. Start the dev loop with `lunora dev` (ask the user to run it locally, or
35
+ start it in the background for cloud/headless agents — it is long-running and
36
+ does not exit).
37
+ 6. Verify a query/mutation round-trip works end to end.
38
+
39
+ ## Path 1: New Project (Recommended)
40
+
41
+ `lunora init` fetches a whole-project template (frontend + worker entry + Vite
42
+ plugin + `lunora/` already wired together).
43
+
44
+ ```bash
45
+ lunora init my-app --template vite
46
+ cd my-app
47
+ pnpm install
48
+ ```
49
+
50
+ ### Pick a template
51
+
52
+ | Template | Stack |
53
+ | ---------------------- | ---------------------------------------------- |
54
+ | `vite` | React + Vite (the simplest full-stack starter) |
55
+ | `standalone` | Worker-only Lunora backend, no frontend |
56
+ | `astro` | Astro integration |
57
+ | `nuxt` | Nuxt (Vue) |
58
+ | `sveltekit` | SvelteKit |
59
+ | `tanstack-start-react` | TanStack Start (React) |
60
+ | `tanstack-start-solid` | TanStack Start (Solid) |
61
+
62
+ If the user has not specified a preference, default to `vite`. Pass `--template`
63
+ explicitly to avoid the interactive prompt. Templates are fetched remotely (via
64
+ `giget`) from `gh:anolilab/lunora/templates/<type>`; pass `--from <dir>` to use a
65
+ local template directory offline.
66
+
67
+ ### Generate types and push the first run
68
+
69
+ Run this yourself — it is one-shot and exits cleanly:
70
+
71
+ ```bash
72
+ lunora codegen
73
+ ```
74
+
75
+ It writes `lunora/_generated/` and typechecks your schema + functions. Read its
76
+ output to find out whether the code you just wrote is valid.
77
+
78
+ ### Start the dev loop
79
+
80
+ ```bash
81
+ lunora dev
82
+ ```
83
+
84
+ `lunora dev` runs the Vite dev server with the Cloudflare Worker on the same
85
+ origin, plus codegen-on-save and the Lunora Studio. It is long-running and does
86
+ not exit, so:
87
+
88
+ - **Local development (user at the keyboard):** ask the user to run `lunora dev`
89
+ in a terminal.
90
+ - **Cloud or headless agents:** start `lunora dev` in the background.
91
+
92
+ Vite serves on `http://localhost:5173` by default; the Worker is served on the
93
+ same origin via `@cloudflare/vite-plugin`.
94
+
95
+ ## Path 2: Add Lunora to an Existing App
96
+
97
+ Use this when the user already has a Vite-based frontend and wants Lunora as the
98
+ backend.
99
+
100
+ ```bash
101
+ lunora init --here
102
+ ```
103
+
104
+ This finds the existing `vite.config.*` (or creates a minimal one), patches in
105
+ the Lunora Vite plugin, and scaffolds a starter `lunora/`. Then run
106
+ `lunora codegen` and `lunora dev` as above.
107
+
108
+ ### Wire up the client provider
109
+
110
+ Create the `LunoraClient` once at module scope (never inside a component) and
111
+ wrap the app with the framework provider. React example:
112
+
113
+ ```tsx
114
+ // src/client/main.tsx
115
+ import { LunoraClient } from "@lunora/client";
116
+ import { LunoraProvider } from "@lunora/react";
117
+ import { StrictMode } from "react";
118
+ import { createRoot } from "react-dom/client";
119
+
120
+ import { App } from "./App";
121
+
122
+ // @cloudflare/vite-plugin serves the Worker on the same origin as Vite.
123
+ const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
124
+ const client = new LunoraClient({ url });
125
+
126
+ createRoot(document.querySelector("#root")!).render(
127
+ <StrictMode>
128
+ <LunoraProvider client={client}>
129
+ <App />
130
+ </LunoraProvider>
131
+ </StrictMode>,
132
+ );
133
+ ```
134
+
135
+ Vue, Solid, and Svelte have matching providers in `@lunora/vue`, `@lunora/solid`,
136
+ and `@lunora/svelte`. `VITE_LUNORA_URL` is optional — it defaults to
137
+ `location.origin`, which is correct for the single-origin dev setup.
138
+
139
+ ## Writing Your First Function
140
+
141
+ Create a schema and a query/mutation to verify the full loop.
142
+
143
+ `lunora/schema.ts`:
144
+
145
+ ```ts
146
+ import { defineSchema, defineTable, v } from "@lunora/server";
147
+
148
+ export default defineSchema({
149
+ todos: defineTable({
150
+ text: v.string(),
151
+ done: v.boolean(),
152
+ createdAt: v.number(),
153
+ }).index("by_creation", ["createdAt"]),
154
+ });
155
+ ```
156
+
157
+ `lunora/todos.ts`:
158
+
159
+ ```ts
160
+ import type { Id } from "@lunora/server";
161
+ import { mutation, query, v } from "@lunora/server";
162
+
163
+ export const list = query.query(async ({ ctx }) => ctx.db.query("todos").withIndex("by_creation").collect());
164
+
165
+ export const add = mutation
166
+ .input({ text: v.string() })
167
+ .mutation(async ({ ctx, args: { text } }): Promise<Id<"todos">> => ctx.db.insert("todos", { text, done: false, createdAt: Date.now() }));
168
+ ```
169
+
170
+ Run `lunora codegen`, then use it in a component. The `api` object and `Doc` /
171
+ `Id` types come from `lunora/_generated/`:
172
+
173
+ ```tsx
174
+ import { useMutation, useQuery } from "@lunora/react";
175
+
176
+ import { api } from "../../lunora/_generated/api";
177
+ import type { Doc } from "../../lunora/_generated/dataModel";
178
+
179
+ function Todos() {
180
+ const todos = useQuery(api.todos.list, {}) as Doc<"todos">[] | undefined;
181
+ const { mutate: add, pending } = useMutation(api.todos.add);
182
+
183
+ return (
184
+ <div>
185
+ <button disabled={pending} onClick={() => add({ text: "New todo" })}>
186
+ Add
187
+ </button>
188
+ {todos?.map((t) => (
189
+ <div key={t._id}>{t.text}</div>
190
+ ))}
191
+ </div>
192
+ );
193
+ }
194
+ ```
195
+
196
+ `useQuery` opens a live subscription: the list re-renders the instant any
197
+ mutation changes the queried rows.
198
+
199
+ ## Development vs Production
200
+
201
+ Use `lunora dev` during development. When ready to ship:
202
+
203
+ ```bash
204
+ lunora deploy
205
+ ```
206
+
207
+ `lunora deploy` runs codegen, the schema-drift gate, and `wrangler deploy`. Do
208
+ not use it during day-to-day development.
209
+
210
+ Before deploying, run the preflight:
211
+
212
+ ```bash
213
+ lunora doctor
214
+ ```
215
+
216
+ It checks `wrangler.jsonc` (the `SHARD` durable-object binding), D1 placeholder
217
+ ids, `.dev.vars` secrets, and container exports.
218
+
219
+ ## Next Steps
220
+
221
+ - Add authentication: use the `lunora-setup-auth` skill.
222
+ - Add a prebuilt capability (mail, presence, storage, rate limit, crons):
223
+ `lunora registry add <item>` (see `lunora registry list`). For capabilities
224
+ with a dedicated skill, use it: `lunora-setup-mail`, `lunora-setup-storage`,
225
+ `lunora-setup-scheduler`. See the `lunora` router's capability entry for the
226
+ full routing.
227
+ - Build your own reusable capability: use the `lunora-create-package` skill.
228
+ - Plan a schema change: use the `lunora-migration-helper` skill.
229
+ - Scaffold more functions: `vis generate lunora-query --name=listMessages`,
230
+ `lunora-mutation`, `lunora-action`, `lunora-table`, `lunora-cron` (always use
231
+ the `--name=value` form).
232
+
233
+ ## Checklist
234
+
235
+ - [ ] Determined starting point: new project or existing app.
236
+ - [ ] New project: scaffolded with `lunora init --template <t>`.
237
+ - [ ] Existing app: ran `lunora init --here` and wired `LunoraProvider`.
238
+ - [ ] Ran `lunora codegen`: `lunora/_generated/` exists and typecheck is clean.
239
+ - [ ] `lunora dev` is running — user terminal, or background for cloud agents.
240
+ - [ ] Verified a query/mutation round-trip re-renders the client live.
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: lunora-realtime
3
+ description: Wires Lunora's live data into a client. Use for `LunoraClient`/`LunoraProvider`,
4
+ reactive `useQuery`/`useSubscription`, `useMutation` with optimistic updates,
5
+ pagination, connection status, the React/Vue/Solid/Svelte adapters, and the
6
+ `@lunora/db` TanStack binding.
7
+ ---
8
+
9
+ # Lunora Realtime
10
+
11
+ Consume Lunora functions reactively from the client. Queries are live
12
+ subscriptions over WebSocket — a `useQuery` re-renders the instant a mutation
13
+ changes the rows it reads — and mutations can paint optimistically with
14
+ automatic rollback.
15
+
16
+ ## When to Use
17
+
18
+ - Wiring a frontend to a Lunora backend (React, Vue, Solid, Svelte).
19
+ - Adding optimistic updates, pagination, or presence to the UI.
20
+ - Choosing between live hooks and the `@lunora/db` collection layer.
21
+
22
+ ## When Not to Use
23
+
24
+ - Writing the server functions themselves — that's `lunora-functions`.
25
+ - Initial project/provider scaffolding — that's `lunora-quickstart`.
26
+
27
+ ## Provider Setup
28
+
29
+ Create the `LunoraClient` once at module scope and wrap the app. The client owns
30
+ the WebSocket, the optimistic cache, and the offline queue.
31
+
32
+ ```tsx
33
+ import { LunoraClient } from "@lunora/client";
34
+ import { LunoraProvider } from "@lunora/react";
35
+
36
+ const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
37
+ const client = new LunoraClient({ url });
38
+
39
+ // <LunoraProvider client={client}>…</LunoraProvider>
40
+ ```
41
+
42
+ Vue / Solid / Svelte have matching providers in `@lunora/vue`, `@lunora/solid`,
43
+ `@lunora/svelte`; the hook names and semantics below mirror across them.
44
+
45
+ ## Live Queries
46
+
47
+ `useQuery(reference, args)` opens a subscription and returns the value, or
48
+ `undefined` while it loads. The reference comes from codegen
49
+ (`api.<file>.<name>`).
50
+
51
+ ```tsx
52
+ import { useQuery } from "@lunora/react";
53
+
54
+ import { api } from "../../lunora/_generated/api";
55
+ import type { Doc } from "../../lunora/_generated/dataModel";
56
+
57
+ const todos = useQuery(api.todos.list, {}) as Doc<"todos">[] | undefined;
58
+ ```
59
+
60
+ - `undefined` means "loading" — render a skeleton/spinner for it.
61
+ - The subscription tears down on unmount and re-runs only when `args` change or
62
+ the underlying rows change. Keep `args` narrow so a write elsewhere doesn't
63
+ re-push unrelated data (see `lunora-performance-audit`).
64
+ - `useSubscription` is the lower-level primitive for streaming subscriptions;
65
+ `usePreloadedQuery` + `lunoraQueryOptions` support SSR/preload handoff via
66
+ `@lunora/react/server`.
67
+
68
+ ## Authorization & Live Queries
69
+
70
+ Subscriptions re-run the query handler **server-side under anonymous
71
+ identity**. The one-shot `fetch` RPC behind the initial load carries the
72
+ caller's identity, but the live WebSocket channel (the subscription seed and
73
+ every write-driven refresh) does not — it evaluates as anonymous.
74
+
75
+ This matters for any query that authorizes or filters on the authenticated
76
+ user:
77
+
78
+ - A query guarded by `.use(rls(...))` or one that reads `ctx.auth.userId`
79
+ directly returns the user's rows on the **initial** HTTP fetch, but its
80
+ **live** updates evaluate anonymously and may resolve to an empty/denied
81
+ set.
82
+ - This **fails closed** — the live channel shows _less_ data, never another
83
+ user's data, so there is no leak. But it is a correctness caveat: the
84
+ initial render and the live updates can disagree.
85
+
86
+ The supported pattern today is to scope per-user data **outside** of
87
+ `ctx.auth` inside a subscribed query:
88
+
89
+ - Partition the data by shard with `.shardBy(userId)` (or tenant/room), so the
90
+ subscription is already scoped to the right state, or
91
+ - Pass the identifier as an **explicit query arg**
92
+ (`useQuery(api.todos.list, { userId })`) and filter on the arg rather than on
93
+ `ctx.auth`.
94
+
95
+ Reserve `rls()` / `ctx.auth`-based filtering for non-subscribed reads (one-shot
96
+ actions/queries) where identity is always present.
97
+
98
+ ## Mutations + Optimistic Updates
99
+
100
+ `useMutation(reference)` returns `{ mutate, pending }`. Pass an `optimistic`
101
+ callback to paint the next state immediately; if the server rejects the call the
102
+ runtime rolls the cache back automatically.
103
+
104
+ ```tsx
105
+ import { useMutation } from "@lunora/react";
106
+
107
+ import type { Doc, Id } from "../../lunora/_generated/dataModel";
108
+
109
+ const { mutate: add, pending } = useMutation(api.todos.add);
110
+
111
+ await add(
112
+ { text },
113
+ {
114
+ optimistic: (current) => {
115
+ const list = (current as Doc<"todos">[] | undefined) ?? [];
116
+ const provisional: Doc<"todos"> = {
117
+ _id: `optimistic_${Date.now()}` as Id<"todos">,
118
+ _creationTime: Date.now(),
119
+ text,
120
+ done: false,
121
+ createdAt: Date.now(),
122
+ };
123
+ return [provisional, ...list];
124
+ },
125
+ },
126
+ );
127
+ ```
128
+
129
+ - The `optimistic` callback receives the current cached value and returns the
130
+ provisional one. When the server delta arrives it replaces the optimistic
131
+ entry; on failure the cache reverts.
132
+ - `pending` is `true` while the call is in flight — disable the submit button
133
+ with it.
134
+ - **Offline queue:** mutations made while disconnected are queued by
135
+ `LunoraClient` and replayed on reconnect (client-id-keyed, so they aren't
136
+ double-applied).
137
+
138
+ ## Pagination, Connection, Presence
139
+
140
+ - `usePaginatedQuery` / `useInfiniteQuery` — cursor pagination over a query that
141
+ ends in `.paginate(...)` on the server.
142
+ - `useConnectionStatus` — live socket state for an offline/reconnecting banner.
143
+ - `usePresence` — who's-here + heartbeat (pairs with the `presence` registry
144
+ item).
145
+ - `useAuth` + the `Authenticated` / `Unauthenticated` / `AuthLoading` gates —
146
+ see `lunora-setup-auth`.
147
+
148
+ ## `@lunora/db` — TanStack DB Collections
149
+
150
+ For richer client state (indexed local collections, cross-query joins, a durable
151
+ offline-transactions outbox), use `@lunora/db` instead of raw hooks. Scaffold
152
+ with `vis generate lunora-collections` (wires `defineCollections` from your
153
+ schema + functions into live TanStack DB collections). Reach for it when the app
154
+ needs client-side indexes/joins or a persistent optimistic outbox; raw `useQuery`
155
+ /`useMutation` are enough for straightforward live lists.
156
+
157
+ ## Common Pitfalls
158
+
159
+ 1. **Creating `LunoraClient` inside a component.** It re-opens the socket every
160
+ render — create it once at module scope.
161
+ 2. **Treating `undefined` as empty.** `undefined` is "loading", `[]` is "loaded,
162
+ empty" — branch on both.
163
+ 3. **Broad query args.** A subscription keyed too broadly re-renders on
164
+ unrelated writes; scope `args` to what the component shows.
165
+ 4. **Optimistic shape drift.** The provisional value must match the query's
166
+ element shape (including `_id`/`_creationTime`) or the UI flickers when the
167
+ real delta lands.
168
+
169
+ ## Checklist
170
+
171
+ - [ ] `LunoraClient` created once at module scope; app wrapped in the provider.
172
+ - [ ] Live reads use `useQuery`; `undefined` handled as loading.
173
+ - [ ] Writes use `useMutation`; `pending` disables submit; `optimistic` matches
174
+ the row shape.
175
+ - [ ] Query `args` scoped narrowly to avoid over-broad re-renders.
176
+ - [ ] Pagination via `usePaginatedQuery`/`useInfiniteQuery` over `.paginate`.
177
+ - [ ] Considered `@lunora/db` if the app needs local indexes/joins or an outbox.
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: lunora-setup-auth
3
+ description: Adds authentication to a Lunora app. Use for sign-up/sign-in, email/password,
4
+ OAuth (Clerk, Auth0), magic link, or email OTP via `lunora registry add auth`,
5
+ wiring the auth handler into the Worker, and gating functions on the session.
6
+ ---
7
+
8
+ # Lunora Setup Auth
9
+
10
+ Wire authentication into a Lunora app using the `auth` registry item, which is
11
+ built on `@lunora/auth` (a thin wrapper over
12
+ [better-auth](https://www.better-auth.com)) with sessions persisted in
13
+ `SessionDO` and identity tables in D1.
14
+
15
+ ## When to Use
16
+
17
+ - Adding sign-up / sign-in to a Lunora app.
18
+ - Adding an OAuth/OIDC provider (Clerk, Auth0), magic link, or email OTP.
19
+ - Gating queries/mutations on the signed-in user.
20
+
21
+ ## When Not to Use
22
+
23
+ - The project has no Lunora backend yet — use `lunora-quickstart` first.
24
+ - You only need to read `ctx.auth.userId` in a function and auth is already
25
+ installed — just use it.
26
+
27
+ ## Workflow
28
+
29
+ 1. Add the base `auth` item.
30
+ 2. Mount the auth request handler in the Worker entry.
31
+ 3. Configure env vars and the D1 database.
32
+ 4. (Optional) Layer a provider item (Clerk / Auth0 / magic link / OTP) on top.
33
+ 5. Gate functions on `ctx.auth.userId`; gate UI with the auth gates/hooks.
34
+
35
+ ## Step 1: Add the base item
36
+
37
+ ```bash
38
+ lunora registry add auth
39
+ ```
40
+
41
+ This:
42
+
43
+ 1. Adds `@lunora/auth`, `@lunora/mail`, and `@lunora/server` to `package.json`
44
+ (run `pnpm install` afterwards).
45
+ 2. Copies `lunora/auth/index.ts` — the auth instance (`buildAuth` / `getAuth`)
46
+ and the `/api/auth/*` request handler (`mountAuth`) — into your project. It is
47
+ **yours** to edit.
48
+ 3. Adds a D1 `DB` binding to `wrangler.jsonc` (better-auth persists
49
+ users/sessions there).
50
+ 4. Scaffolds `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, and `MAIL_FROM` into
51
+ `.dev.vars`.
52
+
53
+ ## Step 2: Mount the handler
54
+
55
+ In your Worker entry, route `/api/auth/*` to the scaffolded handler (see the
56
+ generated `lunora/auth/index.ts` README block). `createWorker` handles the rest
57
+ of the RPC surface; the auth handler owns the better-auth endpoints.
58
+
59
+ ## Step 3: Env vars and the D1 database
60
+
61
+ | Var | Secret | Notes |
62
+ | -------------------- | ------ | ------------------------------------------------------------------- |
63
+ | `BETTER_AUTH_SECRET` | yes | Encryption secret, min 32 chars. `openssl rand -base64 32`. |
64
+ | `BETTER_AUTH_URL` | no | Public base URL, e.g. `http://localhost:8787` in dev, your domain. |
65
+ | `MAIL_FROM` | no | Sender for verification / reset mail. Captured in the dev Mail tab. |
66
+
67
+ Create the D1 database and paste its id into the `DB` binding in
68
+ `wrangler.jsonc`:
69
+
70
+ ```bash
71
+ wrangler d1 create my-app-db
72
+ ```
73
+
74
+ The better-auth schema (user/session/account/verification tables) is **not**
75
+ declared in `lunora/schema.ts` — it is managed by better-auth in D1. In dev,
76
+ `ensureMigrated(auth)` auto-applies it; in production prefer
77
+ `compileMigrationsSql(auth.options)` piped to `wrangler d1 execute`. Run
78
+ `lunora doctor` to confirm the `DB` binding has a real `database_id` (not a
79
+ placeholder).
80
+
81
+ Verification and password-reset emails are **captured into the Lunora Studio
82
+ Mail tab** in dev with zero email setup. For real delivery, `lunora registry add
83
+ mail` (adds the `SEND_EMAIL` binding) or set `RESEND_API_KEY`.
84
+
85
+ ## Step 4: Add a provider (optional)
86
+
87
+ Each provider item builds on the base `auth` item (`requires: ["auth"]`):
88
+
89
+ ```bash
90
+ lunora registry add auth-clerk # Clerk via better-auth genericOAuth
91
+ lunora registry add auth-auth0 # Auth0 via better-auth genericOAuth
92
+ lunora registry add auth-magic-link # passwordless magic-link (mail)
93
+ lunora registry add auth-otp # passwordless email one-time-password
94
+ ```
95
+
96
+ Add the base `auth` item first (or let the registry resolve the `requires`
97
+ dependency). OAuth items need the provider's client id/secret added to
98
+ `.dev.vars`.
99
+
100
+ ## Step 5: Use the session
101
+
102
+ ### In functions
103
+
104
+ The runtime resolves the session and exposes the user on every context:
105
+
106
+ ```ts
107
+ import { LunoraError, mutation, v } from "@lunora/server";
108
+
109
+ export const createDocument = mutation.input({ title: v.string() }).mutation(async ({ ctx, args: { title } }) => {
110
+ if (!ctx.auth.userId) {
111
+ throw new LunoraError("UNAUTHORIZED", "not signed in");
112
+ }
113
+ return ctx.db.insert("documents", { ownerId: ctx.auth.userId, title, createdAt: Date.now() });
114
+ });
115
+ ```
116
+
117
+ For richer checks (org membership, roles), compose `withAuthPlugins(auth)` and
118
+ call the better-auth server API — see the scaffolded `lunora/auth/index.ts`.
119
+
120
+ ### In the UI (React)
121
+
122
+ ```tsx
123
+ import { Authenticated, Unauthenticated, useAuth } from "@lunora/react";
124
+
125
+ function Account() {
126
+ const { user, signIn, signOut } = useAuth();
127
+
128
+ return (
129
+ <>
130
+ <Authenticated>
131
+ <span>Signed in as {user?.email}</span>
132
+ <button type="button" onClick={() => signOut()}>
133
+ Sign out
134
+ </button>
135
+ </Authenticated>
136
+ <Unauthenticated>
137
+ <button type="button" onClick={() => signIn()}>
138
+ Sign in
139
+ </button>
140
+ </Unauthenticated>
141
+ </>
142
+ );
143
+ }
144
+ ```
145
+
146
+ `@lunora/react` also exports `AuthLoading` and `useAuthState` for the loading
147
+ window before the session resolves.
148
+
149
+ ## Common Pitfalls
150
+
151
+ 1. **Declaring better-auth tables in `lunora/schema.ts`.** They live in D1 and
152
+ are managed by better-auth — do not add them to `defineSchema`.
153
+ 2. **Placeholder `database_id`.** The `DB` binding ships with a placeholder;
154
+ `wrangler d1 create` + paste the id, then `lunora doctor` to confirm.
155
+ 3. **Missing/short `BETTER_AUTH_SECRET`.** better-auth needs ≥32 chars;
156
+ `@lunora/auth` surfaces a clear error when it is absent.
157
+ 4. **Expecting prod email to "just work".** Dev captures mail into the Studio;
158
+ production needs `mail` (the `SEND_EMAIL` binding) or `RESEND_API_KEY` and a
159
+ verified sender domain.
160
+
161
+ ## Checklist
162
+
163
+ - [ ] `lunora registry add auth` run, `pnpm install` done.
164
+ - [ ] Auth handler mounted in the Worker entry.
165
+ - [ ] `BETTER_AUTH_SECRET` / `BETTER_AUTH_URL` / `MAIL_FROM` set in `.dev.vars`.
166
+ - [ ] D1 database created and its id pasted into the `DB` binding (`lunora
167
+ doctor` clean).
168
+ - [ ] Provider item added if needed (Clerk / Auth0 / magic link / OTP).
169
+ - [ ] Functions gate on `ctx.auth.userId`; UI uses the auth gates/`useAuth`.
170
+ - [ ] Verified sign-in → session → an authenticated query round-trip.