@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.
- package/LICENSE.md +105 -0
- package/README.md +109 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/bin.mjs +11 -0
- package/dist/index.d.mts +852 -0
- package/dist/index.d.ts +852 -0
- package/dist/index.mjs +19 -0
- package/dist/packem_chunks/handler.mjs +76 -0
- package/dist/packem_chunks/handler10.mjs +22 -0
- package/dist/packem_chunks/handler11.mjs +192 -0
- package/dist/packem_chunks/handler12.mjs +131 -0
- package/dist/packem_chunks/handler13.mjs +65 -0
- package/dist/packem_chunks/handler14.mjs +58 -0
- package/dist/packem_chunks/handler15.mjs +79 -0
- package/dist/packem_chunks/handler16.mjs +41 -0
- package/dist/packem_chunks/handler17.mjs +105 -0
- package/dist/packem_chunks/handler18.mjs +172 -0
- package/dist/packem_chunks/handler19.mjs +89 -0
- package/dist/packem_chunks/handler2.mjs +114 -0
- package/dist/packem_chunks/handler20.mjs +94 -0
- package/dist/packem_chunks/handler21.mjs +311 -0
- package/dist/packem_chunks/handler3.mjs +204 -0
- package/dist/packem_chunks/handler4.mjs +33 -0
- package/dist/packem_chunks/handler5.mjs +49 -0
- package/dist/packem_chunks/handler6.mjs +91 -0
- package/dist/packem_chunks/handler7.mjs +42 -0
- package/dist/packem_chunks/handler8.mjs +174 -0
- package/dist/packem_chunks/handler9.mjs +16 -0
- package/dist/packem_chunks/planDevCommand.mjs +543 -0
- package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
- package/dist/packem_chunks/runDeployCommand.mjs +504 -0
- package/dist/packem_chunks/runInitCommand.mjs +652 -0
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +397 -0
- package/dist/packem_chunks/runResetCommand.mjs +41 -0
- package/dist/packem_chunks/runRpcCommand.mjs +68 -0
- package/dist/packem_shared/COMMANDS-1V_KEx35.mjs +905 -0
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +244 -0
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +19 -0
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +13 -0
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +38 -0
- package/dist/packem_shared/command-BDXcJCCJ.mjs +14 -0
- package/dist/packem_shared/createLogger-CHPNjFw2.mjs +73 -0
- package/dist/packem_shared/defaultSpawner-DxI3mebw.mjs +43 -0
- package/dist/packem_shared/diffSnapshots-RR2ZE8Ya.mjs +161 -0
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -0
- package/dist/packem_shared/features-ocSSpZtS.mjs +24 -0
- package/dist/packem_shared/insertSchemaExtension-BuzF6-t2.mjs +59 -0
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +41 -0
- package/dist/packem_shared/output-format-7gyGR3h8.mjs +17 -0
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +56 -0
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +94 -0
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +16 -0
- package/dist/packem_shared/runAddCommand-BZGkRnBs.mjs +693 -0
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
- package/dist/packem_shared/schemaIrToSnapshot-aBTo7TM5.mjs +43 -0
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
- package/package.json +61 -18
- package/skills/README.md +29 -0
- package/skills/lunora/SKILL.md +83 -0
- package/skills/lunora-create-package/SKILL.md +129 -0
- package/skills/lunora-deploy/SKILL.md +150 -0
- package/skills/lunora-functions/SKILL.md +182 -0
- package/skills/lunora-migration-helper/SKILL.md +194 -0
- package/skills/lunora-performance-audit/SKILL.md +143 -0
- package/skills/lunora-quickstart/SKILL.md +240 -0
- package/skills/lunora-realtime/SKILL.md +177 -0
- package/skills/lunora-setup-auth/SKILL.md +170 -0
- package/skills/lunora-setup-hyperdrive/SKILL.md +154 -0
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +171 -0
- package/skills/lunora-setup-mail/SKILL.md +151 -0
- package/skills/lunora-setup-scheduler/SKILL.md +157 -0
- 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.
|