@lunora/cli 0.0.0 → 1.0.0-alpha.10
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 +956 -0
- package/dist/index.d.ts +956 -0
- package/dist/index.mjs +19 -0
- package/dist/packem_chunks/handler.mjs +150 -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 +43 -0
- package/dist/packem_chunks/handler17.mjs +105 -0
- package/dist/packem_chunks/handler18.mjs +170 -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 +500 -0
- package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
- package/dist/packem_chunks/runDeployCommand.mjs +504 -0
- package/dist/packem_chunks/runInitCommand.mjs +1498 -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-D3h9Iwvl.mjs +944 -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-BC30oSBW.mjs +14 -0
- package/dist/packem_shared/commands-DPKWlqqX.mjs +812 -0
- package/dist/packem_shared/createLogger-B40gPzQo.mjs +78 -0
- package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +43 -0
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +61 -0
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +161 -0
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -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-wUvAN6AL.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-CTRA_JlL.mjs +4 -0
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +43 -0
- package/dist/packem_shared/storage-2RJBhUC4.mjs +84 -0
- package/dist/packem_shared/tui-prompts-DEiPCKV-.mjs +661 -0
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
- package/package.json +62 -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 +158 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lunora-migration-helper
|
|
3
|
+
description: Plans Lunora schema and data migrations with widen-migrate-narrow. Use for
|
|
4
|
+
breaking schema changes, backfills, table reshaping, online data migrations
|
|
5
|
+
(`defineMigration` + `lunora migrate up`), the `.global()` D1 SQL flow, and the
|
|
6
|
+
pre-deploy schema-drift gate.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Lunora Migration Helper
|
|
10
|
+
|
|
11
|
+
Safely change a Lunora schema and migrate data when making breaking changes.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Adding required fields to existing tables.
|
|
16
|
+
- Changing field types or structure.
|
|
17
|
+
- Splitting/merging tables, renaming/removing fields.
|
|
18
|
+
- Reshaping `.global()` (D1-backed) tables.
|
|
19
|
+
|
|
20
|
+
## When Not to Use
|
|
21
|
+
|
|
22
|
+
- Greenfield schema with no data at rest.
|
|
23
|
+
- Adding **optional** fields that need no backfill.
|
|
24
|
+
- Adding new tables or indexes with no correctness concern.
|
|
25
|
+
|
|
26
|
+
## Two Storage Layers — Know Which You Are Migrating
|
|
27
|
+
|
|
28
|
+
Lunora tables live in one of two backends, and they migrate differently:
|
|
29
|
+
|
|
30
|
+
- **ShardDO SQLite (default `root`, and `.shardBy(key)` tables).** State lives in
|
|
31
|
+
the per-app / per-shard Durable Object. Data is reshaped with **online data
|
|
32
|
+
migrations** — `defineMigration` declarations run by `lunora migrate up`,
|
|
33
|
+
resumable per shard.
|
|
34
|
+
- **`.global()` tables (D1).** Replicated to D1 for cross-region reads. Their
|
|
35
|
+
structural DDL gets versioned **SQL migrations** via `lunora migrate generate`,
|
|
36
|
+
applied by `@lunora/d1`'s runner at deploy time.
|
|
37
|
+
|
|
38
|
+
A breaking change to a `.global()` table needs a generated SQL migration; a data
|
|
39
|
+
backfill (either layer) is an online `defineMigration`. Both follow the same
|
|
40
|
+
**widen → migrate → narrow** discipline.
|
|
41
|
+
|
|
42
|
+
## Key Principle: Widen, Migrate, Narrow
|
|
43
|
+
|
|
44
|
+
The schema-drift gate (and D1 itself) will not let a breaking change deploy
|
|
45
|
+
without an accompanying migration. So every breaking change is staged:
|
|
46
|
+
|
|
47
|
+
1. **Widen** — make the schema accept both old and new shapes (add the new field
|
|
48
|
+
as `v.optional`, keep the old one). Update reads to handle both; start writing
|
|
49
|
+
the new shape for new rows. Deploy.
|
|
50
|
+
2. **Migrate** — backfill existing rows to the new shape (an online
|
|
51
|
+
`defineMigration` run with `lunora migrate up`; plus `lunora migrate generate`
|
|
52
|
+
for `.global()` structural DDL). Verify completeness with `lunora migrate
|
|
53
|
+
status`.
|
|
54
|
+
3. **Narrow** — make the field required / drop the old field, remove the
|
|
55
|
+
both-shapes read code. Deploy.
|
|
56
|
+
|
|
57
|
+
### Prefer new fields over changing types
|
|
58
|
+
|
|
59
|
+
When changing a field's shape, add a new field rather than mutating the existing
|
|
60
|
+
one — safer transition, easier rollback.
|
|
61
|
+
|
|
62
|
+
### Don't delete data prematurely
|
|
63
|
+
|
|
64
|
+
Prefer deprecating: mark the old field `v.optional` with a `// deprecated:` code
|
|
65
|
+
comment explaining why it existed. Delete only once you are sure nothing reads
|
|
66
|
+
it.
|
|
67
|
+
|
|
68
|
+
## Safe Changes (No Migration Needed)
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// Adding an optional field — safe.
|
|
72
|
+
users: defineTable({
|
|
73
|
+
name: v.string(),
|
|
74
|
+
bio: v.optional(v.string()),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Adding a new table — safe.
|
|
78
|
+
posts: defineTable({ userId: v.id("users"), title: v.string() }).index("by_user", ["userId"]);
|
|
79
|
+
|
|
80
|
+
// Adding an index — safe.
|
|
81
|
+
users: defineTable({ name: v.string(), email: v.string() }).index("by_email", ["email"]);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Online Data Migrations (the backfill workhorse)
|
|
85
|
+
|
|
86
|
+
For backfilling/reshaping rows, declare a migration with `defineMigration` from
|
|
87
|
+
`@lunora/server`. It transforms one document at a time, runs **inside each
|
|
88
|
+
shard's** Durable Object in keyset batches, and is **resumable** — per-shard
|
|
89
|
+
progress is tracked in a reserved `__lunora_migrations` table, so an interrupted
|
|
90
|
+
run resumes where it stopped. Codegen discovers declarations and emits them into
|
|
91
|
+
the registry the DO and CLI look up by `id`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// lunora/migrations/backfill-display-name.ts
|
|
95
|
+
import { defineMigration } from "@lunora/server";
|
|
96
|
+
|
|
97
|
+
export default defineMigration({
|
|
98
|
+
id: "backfill-display-name",
|
|
99
|
+
table: "users",
|
|
100
|
+
batchSize: 200, // optional; defaults to the runner's batch size
|
|
101
|
+
up: (doc) => {
|
|
102
|
+
if (typeof doc.displayName === "string") {
|
|
103
|
+
return; // already migrated — return undefined to skip (not counted as changed)
|
|
104
|
+
}
|
|
105
|
+
return { ...doc, displayName: doc.name ?? "Anonymous" };
|
|
106
|
+
},
|
|
107
|
+
// optional reverse transform applied by `lunora migrate down`
|
|
108
|
+
down: (doc) => {
|
|
109
|
+
const { displayName, ...rest } = doc as Record<string, unknown>;
|
|
110
|
+
return rest;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The transform must preserve row identity — the runner always keeps the original
|
|
116
|
+
`_id` / `_creationTime`, so do not change them.
|
|
117
|
+
|
|
118
|
+
### Run it
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
lunora migrate create backfill-display-name # scaffold the migration file
|
|
122
|
+
lunora codegen # discover + register it
|
|
123
|
+
lunora migrate up backfill-display-name --dry-run # preview, no rows rewritten
|
|
124
|
+
lunora migrate up backfill-display-name # run across shards (keyset batches)
|
|
125
|
+
lunora migrate status backfill-display-name # per-shard progress
|
|
126
|
+
lunora migrate down backfill-display-name # revert (if `down` defined)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Useful flags: `--batch-size <n>`, `--steps <n>` (cap batches this run), and
|
|
130
|
+
`--prod --url <worker> --yes` to target production (with `LUNORA_ADMIN_TOKEN`).
|
|
131
|
+
|
|
132
|
+
## `.global()` (D1) Structural Migration Flow
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# 1. Edit lunora/schema.ts (widen: add the optional new field to the .global() table).
|
|
136
|
+
lunora codegen
|
|
137
|
+
|
|
138
|
+
# 2. Generate the SQL migration by diffing schema against the snapshot baseline.
|
|
139
|
+
lunora migrate generate --name=add_user_status
|
|
140
|
+
|
|
141
|
+
# Writes lunora/migrations/<timestamp>_add_user_status.sql and updates
|
|
142
|
+
# lunora/migrations/.snapshot.json. Review the SQL before committing.
|
|
143
|
+
|
|
144
|
+
# 3. Deploy — @lunora/d1's runner applies pending migrations.
|
|
145
|
+
lunora deploy
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`lunora migrate generate` only considers `.global()` tables (root/sharded tables
|
|
149
|
+
are not D1-backed). Run it after each schema edit in the widen and narrow steps;
|
|
150
|
+
backfill data with an online migration between them.
|
|
151
|
+
|
|
152
|
+
## The Schema-Drift Gate
|
|
153
|
+
|
|
154
|
+
`lunora deploy` (and `verify` / `prepare`) run a **pre-deploy schema-drift gate**:
|
|
155
|
+
it compares the committed structural baseline (`lunora/.lunora-schema.json`)
|
|
156
|
+
against the snapshot codegen produced this run. Breaking drift **without an
|
|
157
|
+
accompanying data migration blocks the deploy**. The baseline is only re-blessed
|
|
158
|
+
_after_ the deploy succeeds, so a failed deploy never advances it past a change
|
|
159
|
+
that never shipped.
|
|
160
|
+
|
|
161
|
+
If the gate blocks you: that is the signal to stage the change (widen first) or
|
|
162
|
+
add the migration — not to bypass it.
|
|
163
|
+
|
|
164
|
+
## Common Pitfalls
|
|
165
|
+
|
|
166
|
+
1. **Making a field required before backfilling.** The drift gate / D1 rejects
|
|
167
|
+
the deploy because existing rows lack it. Widen first.
|
|
168
|
+
2. **Reshaping rows by hand instead of `defineMigration`.** A hand-rolled
|
|
169
|
+
`internalMutation` that `.collect()`s a large table hits transaction limits
|
|
170
|
+
and is not resumable. Use `defineMigration` — it batches and tracks per-shard
|
|
171
|
+
progress.
|
|
172
|
+
3. **Not writing the new shape during the migration window.** Rows created mid-
|
|
173
|
+
migration get missed, leaving unmigrated data after it "completes." Start
|
|
174
|
+
dual-writing in the widen step.
|
|
175
|
+
4. **Skipping the dry run.** `lunora migrate up <id> --dry-run` validates the
|
|
176
|
+
transform before it touches real rows.
|
|
177
|
+
5. **Deleting a field prematurely.** Deprecate with `v.optional` + a comment;
|
|
178
|
+
delete only once nothing references it.
|
|
179
|
+
6. **Migrating the wrong layer.** A `.global()` structural change needs `lunora
|
|
180
|
+
migrate generate` (SQL); a data backfill needs a `defineMigration`. Check the
|
|
181
|
+
table's `.global()` / `.shardBy()` modifier first.
|
|
182
|
+
|
|
183
|
+
## Checklist
|
|
184
|
+
|
|
185
|
+
- [ ] Identified the change and which layer it touches (ShardDO vs `.global()`).
|
|
186
|
+
- [ ] Widened the schema to accept both shapes; `lunora codegen` clean.
|
|
187
|
+
- [ ] Updated reads to handle both shapes; started writing the new shape.
|
|
188
|
+
- [ ] Deployed the widened schema.
|
|
189
|
+
- [ ] Authored a `defineMigration`; previewed with `lunora migrate up --dry-run`.
|
|
190
|
+
- [ ] Ran `lunora migrate up`; `lunora migrate generate` + deploy for `.global()`
|
|
191
|
+
structural changes.
|
|
192
|
+
- [ ] Verified completion with `lunora migrate status`.
|
|
193
|
+
- [ ] Narrowed the schema (required / drop old field); removed both-shapes code.
|
|
194
|
+
- [ ] Deployed the final schema; schema-drift gate passed.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lunora-performance-audit
|
|
3
|
+
description: Diagnoses and fixes Lunora performance problems — full-table scans, missing
|
|
4
|
+
indexes, OCC write conflicts, oversized subscriptions, and sharding/`.global()`
|
|
5
|
+
scaling. Use when queries are slow, mutations conflict, or `@lunora/advisor`
|
|
6
|
+
flags a table.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Lunora Performance Audit
|
|
10
|
+
|
|
11
|
+
Systematically diagnose Lunora performance issues, route to the right fix, and
|
|
12
|
+
apply it across sibling functions consistently.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Queries feel slow or read far more rows than they return.
|
|
17
|
+
- Mutations retry or conflict under load (OCC).
|
|
18
|
+
- Subscriptions fan out updates too broadly or re-run too often.
|
|
19
|
+
- The Lunora Studio **Advisors** tab or `@lunora/advisor` flags a table.
|
|
20
|
+
|
|
21
|
+
## When Not to Use
|
|
22
|
+
|
|
23
|
+
- Scale is small, traffic is modest, and there is no measured problem — prefer
|
|
24
|
+
simpler code. Do not introduce sharding, digests, or splits on speculation.
|
|
25
|
+
|
|
26
|
+
## Core Workflow
|
|
27
|
+
|
|
28
|
+
1. **Scope** one concrete user flow with a clear entry and exit.
|
|
29
|
+
2. **Trace** every `ctx.db` read and write in that flow.
|
|
30
|
+
3. **Route** to the matching problem class below.
|
|
31
|
+
4. **Fix siblings** consistently — every function touching the same table should
|
|
32
|
+
read it the same indexed way.
|
|
33
|
+
5. **Verify** behavior is unchanged and the advisor finding clears.
|
|
34
|
+
|
|
35
|
+
## Signal Gathering
|
|
36
|
+
|
|
37
|
+
Start with the static advisors — they need no traffic:
|
|
38
|
+
|
|
39
|
+
- **Lunora Studio → Advisors tab** surfaces `@lunora/advisor` findings live in
|
|
40
|
+
dev.
|
|
41
|
+
- `@lunora/advisor` runs static lints over `defineSchema` + discovered query
|
|
42
|
+
reads / insert writes. Relevant performance/schema rules:
|
|
43
|
+
- `filter-without-index` — a query filters a table with no covering index.
|
|
44
|
+
- `unindexed-foreign-key` — a relation/FK column has no index.
|
|
45
|
+
- `duplicate-index` / `empty-index` — wasted or malformed indexes.
|
|
46
|
+
- `index-references-unknown-field`, `relation-references-unknown-field`,
|
|
47
|
+
`relation-references-unknown-table` — broken index/relation definitions.
|
|
48
|
+
- `table-without-insert` — a table is read but never written (often a
|
|
49
|
+
schema/typo signal).
|
|
50
|
+
|
|
51
|
+
## Problem Class: Read Amplification (the common one)
|
|
52
|
+
|
|
53
|
+
**Symptom:** a query reads the whole table to return a few rows;
|
|
54
|
+
`filter-without-index` fires.
|
|
55
|
+
|
|
56
|
+
**Fix:** read through an index, not `.filter()`. Declare the index on the table
|
|
57
|
+
and use `.withIndex()` with an equality/range on the leading columns.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// Bad — scans every row, then filters in memory.
|
|
61
|
+
const mine = (await ctx.db.query("documents").collect()).filter((d) => d.orgId === orgId);
|
|
62
|
+
|
|
63
|
+
// Good — declare the index…
|
|
64
|
+
documents: defineTable({ orgId: v.string(), createdAt: v.number() /* … */ }).index("by_org_created", ["orgId", "createdAt"]);
|
|
65
|
+
|
|
66
|
+
// …and read through it.
|
|
67
|
+
const mine = await ctx.db
|
|
68
|
+
.query("documents")
|
|
69
|
+
.withIndex("by_org_created", (q) => q.eq("orgId", orgId))
|
|
70
|
+
.collect();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Index columns are ordered: put equality columns first, then the range/sort
|
|
74
|
+
column. Fix every sibling query on the table the same way.
|
|
75
|
+
|
|
76
|
+
## Problem Class: Write Conflicts (OCC)
|
|
77
|
+
|
|
78
|
+
**Symptom:** mutations on hot rows retry or fail under concurrency. ShardDO uses
|
|
79
|
+
optimistic concurrency control — concurrent writes to the same DO that touch
|
|
80
|
+
overlapping state conflict and retry.
|
|
81
|
+
|
|
82
|
+
**Fixes, in order of preference:**
|
|
83
|
+
|
|
84
|
+
1. **Narrow the write.** Patch only the fields that changed; avoid read-modify-
|
|
85
|
+
write over rows another mutation also touches.
|
|
86
|
+
2. **Partition with `.shardBy(key)`.** Move per-user / per-tenant / per-room
|
|
87
|
+
state into its own DO so writes for different keys never contend. This is the
|
|
88
|
+
primary horizontal-scale lever — most write contention is a single-DO
|
|
89
|
+
hotspot.
|
|
90
|
+
3. **Avoid unbounded counters/aggregates in the hot path.** Accumulate in a
|
|
91
|
+
sharded/append shape and fold lazily rather than serializing every writer
|
|
92
|
+
through one row.
|
|
93
|
+
|
|
94
|
+
## Problem Class: Subscription Cost
|
|
95
|
+
|
|
96
|
+
**Symptom:** a `useQuery` re-runs and re-pushes to many clients on unrelated
|
|
97
|
+
writes.
|
|
98
|
+
|
|
99
|
+
**Fixes:**
|
|
100
|
+
|
|
101
|
+
- **Scope query args tightly** so a subscription only depends on the rows it
|
|
102
|
+
renders — a query keyed by `orgId` should not re-run for another org's write.
|
|
103
|
+
- **Read through indexes** (above) so the reactive dependency is the narrow
|
|
104
|
+
index range, not the whole table.
|
|
105
|
+
- ShardDO subscriptions are **hibernated WebSockets** — idle connections cost
|
|
106
|
+
nothing; the lever is _how many rows each live query depends on_, not raw
|
|
107
|
+
connection count.
|
|
108
|
+
|
|
109
|
+
## Problem Class: Cross-Region Reads
|
|
110
|
+
|
|
111
|
+
**Symptom:** read-heavy, rarely-written data is slow for far-away users.
|
|
112
|
+
|
|
113
|
+
**Fix:** chain `.global()` on the table to replicate it to D1 for low-latency
|
|
114
|
+
cross-region reads (with read-your-writes via the Sessions API). Reserve it for
|
|
115
|
+
read-mostly tables — `.global()` adds the D1 migration flow (see the
|
|
116
|
+
`lunora-migration-helper` skill) and write-path cost.
|
|
117
|
+
|
|
118
|
+
### `.shardBy(key)` vs `.global()` — choose one per table
|
|
119
|
+
|
|
120
|
+
- `.shardBy(key)`: partitions a table across Durable Objects by key — scales
|
|
121
|
+
_writes_ (e.g. messages per room). Reads are per-shard.
|
|
122
|
+
- `.global()`: replicates a table to D1 — scales _cross-region reads_ with
|
|
123
|
+
read-your-writes (e.g. a mostly-read catalog).
|
|
124
|
+
- They are not combined on the same table; the default (neither) is a single
|
|
125
|
+
root-scoped ShardDO.
|
|
126
|
+
|
|
127
|
+
## Guardrails
|
|
128
|
+
|
|
129
|
+
- Prefer simpler code when scale is small or the signal is weak.
|
|
130
|
+
- Do not recommend structural changes (digest tables, document splitting,
|
|
131
|
+
sharding) without a measured signal or a known hot path.
|
|
132
|
+
- When you change how one function reads/writes a table, change its siblings to
|
|
133
|
+
match — half-migrated access patterns are their own bug.
|
|
134
|
+
|
|
135
|
+
## Checklist
|
|
136
|
+
|
|
137
|
+
- [ ] Scoped one concrete flow; traced every `ctx.db` read/write.
|
|
138
|
+
- [ ] Checked the Studio Advisors tab / `@lunora/advisor` findings.
|
|
139
|
+
- [ ] Read amplification: replaced `.filter()` with an indexed `.withIndex()`.
|
|
140
|
+
- [ ] Write conflicts: narrowed writes and/or partitioned with `.shardBy(key)`.
|
|
141
|
+
- [ ] Subscription cost: scoped query args so live queries depend on few rows.
|
|
142
|
+
- [ ] Cross-region: applied `.global()` only to read-mostly tables.
|
|
143
|
+
- [ ] Fixed sibling functions consistently; verified behavior unchanged.
|
|
@@ -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.
|