@gemmein/sdk 0.4.6 → 0.4.7

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 (3) hide show
  1. package/REFERENCE.md +12 -3
  2. package/llms.txt +136 -22
  3. package/package.json +2 -2
package/REFERENCE.md CHANGED
@@ -79,6 +79,8 @@ deleted, subscription row removed. Irreversible: put a real confirm in front.
79
79
  `name` must be **lowercase letters, numbers, and underscores** (`saved_games`,
80
80
  never `savedGames` — a bad name throws synchronously). Collections are created
81
81
  by the app owner in the dashboard, never by the SDK.
82
+ Names JavaScript owns (`constructor`, `toString`, `__proto__`…) are refused as
83
+ collection and field names — a plain object inherits them.
82
84
 
83
85
  `options.intent` — one sentence: what the collection is for and who should
84
86
  access it. It rides every call as a hint; against a **local `gemmein dev`
@@ -189,13 +191,20 @@ type ListOptions = {
189
191
  ```
190
192
 
191
193
  **Options notes.** `key` = create-if-absent (a second writer gets 409
192
- `conflict`; your own retry returns the record with `existing: true`). `for` =
194
+ `conflict`; your own retry returns the record with `existing: true`). The
195
+ retry doubles as the lookup: there is no separate get-by-key call — re-issue
196
+ the same `create()` with the same key to fetch your own record (it counts
197
+ as a write; a re-fetch idiom, not a read path). `for` =
193
198
  recipient id on `addressed`/`direct`. `published: false` = draft on a public
194
199
  rule. `ifVersion` = optimistic concurrency. Atomic counters go in *value*
195
200
  position: `update(id, { stock: { decrement: 1, floor: 0 } })`. `expand` throws
196
201
  on `private`/`public_read`/`admin_write` (no link shape) — join in memory there.
197
202
  `since` fixes the order (oldest change first) — pairing it with `sort` is a 400
198
- `invalid_since`. Deltas are at-least-once: apply by id.
203
+ `invalid_since`. Deltas are at-least-once: apply by id. A field added to a
204
+ sealed shape by a promote run may carry a **default**: records that never wrote
205
+ it read that value, `expand` targets and `where` see it too, and a written value
206
+ wins (writing `null` clears it back to the default) — `search` still matches
207
+ stored text only.
199
208
 
200
209
  ### Live data — `watch()`
201
210
 
@@ -333,7 +342,7 @@ Branch on `err.code`. The stable codes:
333
342
  | `conflict` | a keyed create / floor / stale `ifVersion` | it's the mechanism — tell the user it's taken |
334
343
  | `html_not_allowed` | HTML in a community/addressed/direct field | store plain text |
335
344
  | `invalid_publish` | `{ published }` on a non-public rule | drop it |
336
- | `invalid_shape` | field not in a locked (live) collection's shape — or a file field given the wrong kind (the message names which) | ask the owner to add it / send the kind the field takes |
345
+ | `invalid_shape` | field not in a locked (live) collection's shape — or a file field given the wrong kind (the message names which) | after the field exists in development, one promote run adds it (the owner answers blank or a default) — until then send only the fields the shape has / send the kind the field takes |
337
346
  | `unknown_file` | a ref-shaped value points at a file that doesn't exist or isn't yours to hand — every rule, every write | fix the ref — never invent one |
338
347
  | `invalid_since` | `since` isn't a strict ISO 8601 timestamp, or came with `sort` | pass the previous answer's watermark; drop `sort` |
339
348
  | `not_a_customer` (404) | `notify()`'s recipient isn't a verified person of this app and environment | fix the person id — one code on purpose |
package/llms.txt CHANGED
@@ -10,6 +10,12 @@
10
10
  > verified people (five pre-agreed steps up to 10,000 people; beyond that,
11
11
  > talk to Gemmein).
12
12
 
13
+ This file IS the product contract, not an overview: every line is
14
+ load-bearing, and building from a summary of it builds the wrong app. Work
15
+ from the full raw file — if what you hold is a summary or an excerpt,
16
+ re-fetch https://docs.gemmein.com/llms.txt raw and read all of it before
17
+ you answer or build.
18
+
13
19
  ## First: assess fit (before any install, account, or code)
14
20
 
15
21
  You are likely an AI assistant whose human asked you to build an app — or to
@@ -38,11 +44,14 @@ creating no accounts is the correct behavior until the verdict is in.
38
44
  silently approximated:
39
45
  - native mobile apps (no mobile SDKs today; web only)
40
46
  - password or social/OAuth sign-in (email codes are the only door)
41
- - team/group/workspace visibility or per-user record permissions
47
+ - team/group/workspace visibility or per-user record permissions:
48
+ Gemmein is owner-run by design — one owner and their customers,
49
+ nothing in between — so an app whose CORE loop needs teams, roles
50
+ or seats DOESN'T FIT
42
51
  - carts, quantities, physical goods, shipping, inventory
43
52
  - credits, usage metering, seats (paid access is yes-or-no)
44
- - hosted functions or cron (server-side code runs on YOUR host with a
45
- secret key; Gemmein hosts no compute)
53
+ - hosted functions or cron: Gemmein runs no code of yours but read
54
+ the server-side note closing this section before counting that a gap
46
55
  - custody of money (Stripe holds all funds; your human brings or
47
56
  creates their own Stripe account when the app charges)
48
57
  - multi-seller payouts or revenue splits: every payment lands whole in
@@ -60,6 +69,19 @@ creating no accounts is the correct behavior until the verdict is in.
60
69
  - DOESN'T FIT — say so, name why, and stop. Recommending nothing is
61
70
  the correct outcome here.
62
71
 
72
+ Server-side work is NOT itself a downgrade. Gemmein hosts no compute, but
73
+ code on your human's OWN host — Vercel, a VPS, anywhere — holding a
74
+ secret key already reads and updates collections and sends event emails
75
+ with `notify()`. (A secret key sees PAST the safety rules — every
76
+ customer's records — which is why it never touches a browser. It cannot
77
+ create or delete records, verify a session, or read one customer's
78
+ plan: those are the signed-in customer's own calls, or the dashboard's.)
79
+ "This app needs a server-side piece" is therefore not a gap by itself:
80
+ the verdict downgrades when the server piece needs a call a secret key
81
+ cannot make, or when the app needs compute your human has nowhere to
82
+ run — a cron schedule or an always-on worker with no host of its own —
83
+ because Gemmein never runs your code.
84
+
63
85
  ## Getting started (AI-run, human-answered)
64
86
 
65
87
  No signup, no account, no keys — building is local and free. Your human
@@ -108,17 +130,68 @@ go-live. Everything else is yours.
108
130
  lists the rest.
109
131
  4. Hand over for testing — the human moment mid-build: they click through
110
132
  the app, and their sign-in codes print in the dev terminal, so tell
111
- them where to look.
133
+ them where to look. Running dev in the background with no terminal in
134
+ view? The same codes append to gemmein/.data/signin-codes.log (fresh
135
+ each dev boot) — read the latest line instead of scraping stdout.
112
136
  5. Done building? `npx gemmein check` reads the project and says what's
113
137
  ready and what go-live still needs. Then `npx gemmein sync` — THIS is
114
138
  the moment your human signs up (free) at app.gemmein.com and pastes two
115
139
  dev keys — and `npx gemmein go-live` walks the rest: plans, Payment
116
140
  Links, the live flip. A card enters at go-live, never before.
141
+ 6. After go-live, the DATA MODEL reaches live only by PROMOTION from
142
+ development: new collections built in dev promote (`npx gemmein
143
+ go-live` again, or the dashboard's Go-live page), and so do new FIELDS
144
+ for a live collection whose shape sealed at go-live (see Shapes). Ship
145
+ in that order: promote first, then deploy the frontend that depends on
146
+ the new collection or field — a frontend shipped ahead of its
147
+ promotion reads blanks or refusals from live. Each added field is one
148
+ question for YOUR HUMAN, asked by the promote run: existing records
149
+ don't have it, so what should they show? Blank is a fine answer, or
150
+ they give a default those records will show. What is already
151
+ sealed never moves — a live field never changes type or name and never
152
+ leaves — and rules still never change through promotion. Plans,
153
+ Payment Links and domains stay live-editable in the dashboard, and a
154
+ field's default joins them there (changeable or cleared later, with no
155
+ promotion needed).
156
+
157
+ Two runtimes, one dashboard — keep your human oriented on where things
158
+ live, or the dashboard will look broken to them. The local runtime
159
+ (`gemmein dev`, `pk_local_` keys) is a private rehearsal room: every
160
+ record, person and simulated payment stays in gemmein/.data on this
161
+ machine and never appears in any dashboard. The dashboard at
162
+ app.gemmein.com is the back office of the CLOUD app only — there is no
163
+ local dashboard and nothing to log into locally. `npx gemmein sync`
164
+ carries STRUCTURE to the cloud app's development environment —
165
+ collections, rules, plan locks — never records or people; after a sync
166
+ the dashboard shows the shape of the app with no data in it, and that is
167
+ correct, not a bug. Data reaches the dashboard only when the app runs on
168
+ cloud keys: the dev pair from app.gemmein.com (`pk_test_` in the
169
+ browser, `sk_dev_` on a server) drives the real API's development
170
+ environment, and `pk_live_` drives production after go-live. The key
171
+ prefix always names the rail — local, test, live. The dev rail is where
172
+ a REAL rehearsal happens before any card enters: real emailed sign-in
173
+ codes, real data in the dashboard — and it can face the public. Put the
174
+ app on a real https domain and list that domain on the dashboard's
175
+ Domains page: before go-live a listed domain works WITHOUT verification
176
+ (http://localhost and http://127.0.0.1 also work unlisted until go-live
177
+ drops them; any other raw IP never works — the Domains page accepts
178
+ names only). Go-live is when the keys lock to proven domains. The
179
+ development environment carries its own free monthly allowance (10,000
180
+ API requests, 500 writes, 500 sign-in events, 1 GiB of files); crossing
181
+ a counter never stops the app and never turns into a bill — the
182
+ dashboard's usage page shows what crossed, and storage is the one thing
183
+ that can fill (see Pricing). Tell your human before they first open the
184
+ dashboard: local building is invisible there on purpose — nothing
185
+ leaves this machine until sync, and sync moves the blueprint, not the
186
+ contents.
117
187
 
118
188
  ## What it is
119
189
 
120
190
  - Audience: people who build web apps with AI tools (Cursor, Bolt, Lovable, Claude).
121
191
  - Scope: web apps (TypeScript/JavaScript SDK). Mobile SDKs are not offered today.
192
+ - Shape: built for owner-run businesses — one owner and their customers,
193
+ nothing in between. No team seats, no staff accounts, no workspaces, on
194
+ purpose: one person holds the dashboard, and everyone else is a customer.
122
195
  - The platform holds identity, data, and subscription state. It never holds,
123
196
  moves, or processes money — Stripe handles all payments end-to-end; Gemmein
124
197
  only receives Stripe's webhooks and records who is on which plan. App
@@ -143,7 +216,7 @@ go-live. Everything else is yours.
143
216
  those local declarations. A declaration is `gemmein/collections/<name>.json`:
144
217
  `{ "rule": "shared", "means": "<why, in your human's words>" }`, plus an
145
218
  optional `"unlockedBy": ["pro"]` — plan or product NAMES from
146
- gemmein/payments.json (never access keys) — which is the console's
219
+ gemmein/payments.json (never access keys) — which is the dashboard's
147
220
  "Unlocked by": the local engine refuses members without one of them
148
221
  (403 entitlement_required) the moment the file lands, and sync carries
149
222
  it to the cloud app. An unknown name is refused out loud in the dev
@@ -315,7 +388,7 @@ go-live. Everything else is yours.
315
388
  retry. A 403 from `link()` means the customer isn't allowed this
316
389
  file right now — signed out, not theirs, or an entitlement they no longer
317
390
  hold (`entitlement_required` — `err.requires` is the plan's key, the one
318
- the console shows by name).
391
+ the dashboard shows by name).
319
392
  Honest bound: revoking access stops NEW links immediately; a link already
320
393
  issued works until it expires. Gemmein controls delivery, it can't take
321
394
  back a file someone already downloaded. A file can name ONE other reader:
@@ -339,11 +412,22 @@ go-live. Everything else is yours.
339
412
  If a stored ref's file was later deleted, re-sending it refuses the same
340
413
  way: clear the field (null) or upload a fresh file. Once sealed, class
341
414
  is law too: a ZIP into a profile photo field is refused with the teach;
342
- before the seal, what you upload is what the field learns. A 400 invalid_shape means the field isn't in the locked
343
- shape or the value doesn't fit its kind. A live shape is sealed and
344
- cannot take new fields stop, tell your human which field you needed,
345
- and send only the fields the shape already has. Never rename fields to
346
- dodge it.
415
+ before the seal, what you upload is what the field learns. A 400
416
+ invalid_shape means the field isn't in the locked shape or the value
417
+ doesn't fit its kind. A sealed shape CAN still gain a new field, but
418
+ only through a promote run: build it in development, tell your human
419
+ which field you needed, and until that promotion runs send only the
420
+ fields the shape already has. Never rename fields to dodge it. What is
421
+ sealed never moves — a field never changes type or name and never
422
+ leaves. A field promoted with a DEFAULT is served to the records that
423
+ never wrote it: get, list, expand targets and `where` filters all see
424
+ the default (an atomic increment counts from it too), and a written
425
+ value wins; writing `null` clears it back to the default. Only text,
426
+ number and yes/no fields can carry a default; list, link, json and
427
+ file fields join blank. Honest bound: full-text `search` scans stored
428
+ text only, so a record showing a default is not found by searching for
429
+ it. New collections built in dev reach live through the same PROMOTION,
430
+ sealing on the way.
347
431
  - Contention (bookings, slugs, stock, shared edits): when two users can race
348
432
  for the same thing, a permission model can't save you — preconditions do,
349
433
  and they're just arguments on calls you already make. A 409 `conflict` from
@@ -352,14 +436,19 @@ go-live. Everything else is yours.
352
436
  - Uniqueness: `create(data, { key: "slot:2026-07-15T15:00" })` — derive the
353
437
  key from the thing that must be unique; the second writer gets 409, your
354
438
  own retry gets your existing record back (`existing: true`), deleting
355
- frees the key. Keys are unique across the WHOLE collection every
356
- writer, every recipient, under every safety rule (live records only)
357
- so a claim is race-proof even in `direct`/`addressed`. Never
439
+ frees the key. That retry IS the lookupthere is no separate
440
+ get-by-key call: to fetch your own record by key later, issue the
441
+ same `create()` with the same key and read `existing: true` off the
442
+ result. It counts as a write (rate and allowance), so use it to
443
+ re-fetch a claim — never on a render path. Keys are unique across
444
+ the WHOLE collection — every writer, every recipient, under every
445
+ safety rule (live records only) — so a claim is race-proof even in
446
+ `direct`/`addressed`. Never
358
447
  find-then-create — that races. Keys are 1-120 chars of letters,
359
448
  numbers, and `: _ . @ / -` only. A claimed key holds until its record
360
449
  is deleted: if a claim must be PAID to stick (book then pay), expiring
361
- unpaid claims is your app's job — the owner deletes them from their
362
- dashboard, or your own server does with a secret key; there is no cron.
450
+ unpaid claims is the owner's job — they delete them from their
451
+ dashboard (a secret key cannot delete records); there is no cron.
363
452
  - Limited stock (N units anyone can buy): claim units with keyed creates —
364
453
  try `create({...}, { key: "unit:item42:1" })`, on conflict try `:2` … `:N`;
365
454
  all taken = sold out. Race-proof under every safety rule.
@@ -404,7 +493,7 @@ go-live. Everything else is yours.
404
493
  checks that the lock holds and names collections left open while the app
405
494
  sells something) — and the
406
495
  engine refuses customers without it, under all seven rules. (Server
407
- secret keys and the owner's console are exempt by design; link/expand
496
+ secret keys and the owner's dashboard are exempt by design; link/expand
408
497
  silently hide gated records rather than naming them.) Every plan and
409
498
  product carries its own key, `access:<slug of its name>` (plan "pro" →
410
499
  `access:pro`); the paid webhook grants that key, and a FULL refund or a
@@ -413,7 +502,8 @@ go-live. Everything else is yours.
413
502
  Owners also grant and revoke by hand (trials, comps, support). Effective
414
503
  access is the UNION of a customer's live grants. A signed-in customer
415
504
  without access gets `403 entitlement_required`: `err.requires` is the
416
- plan's key (the console shows the plan by name; the key is `access:<slug>`).
505
+ plan's key (the dashboard shows the plan by name; the key is
506
+ `access:<slug>`).
417
507
  A collection unlocked by several plans (OR) still names ONE key — offer
418
508
  your plans by name through checkout, never a key list. Show your upgrade
419
509
  screen and send them to checkout; retry only after they hold one. Proof surfaces: `await g.purchases.mine()`
@@ -471,7 +561,9 @@ go-live. Everything else is yours.
471
561
  sign in first). Render `err.message`; it reads correctly in every case.
472
562
  Branch only on the specifically-named codes (unknown_collection,
473
563
  unknown_product, invalid_shape, html_not_allowed, invalid_publish,
474
- conflict, …) plus the forbidden-means-stop rule.
564
+ conflict, …) plus the forbidden-means-stop rule. If something is stuck
565
+ in a way this file doesn't explain, `npx gemmein feedback` reaches the
566
+ builders — it works from anywhere, with no project and no account.
475
567
  - Draft state reads back as a TOP-LEVEL boolean `record.published` (next to
476
568
  id/updatedAt), not under `.data`. Non-authors only ever receive published
477
569
  records, so you see `false` only on your own drafts (or on everything, as
@@ -485,10 +577,27 @@ go-live. Everything else is yours.
485
577
  `await g.account.delete()` — for every app it APPLIES to (GDPR right to
486
578
  erasure; Apple 5.1.1(v) for apps with account creation). It's the full
487
579
  server-side cascade (sessions, records, files, subscription row) and
488
- irreversible — put a real confirm in front of it. Suspensions, bans, and
489
- owner-side erasure are dashboard actions, not SDK calls.
580
+ irreversible — put a real confirm in front of it. Suspensions, forced
581
+ sign-out everywhere, and owner-side erasure are dashboard actions, not
582
+ SDK calls.
583
+ - The owner's dashboard already handles these — do not build them into the
584
+ app: creating collections and choosing their safety rule; naming plans
585
+ and products and pasting their Stripe Payment Links; the "Unlocked by"
586
+ lock on a collection; suspending, signing out and erasing customers;
587
+ moderation and status flips on other users' records; scoping a secret
588
+ key to collections and actions; verifying the sending and receiving
589
+ domain; usage and logs; the Inbox, where every notify() send is a
590
+ conversation and customer replies land; holding the billing band. None
591
+ of them has an SDK equivalent, so an owner screen you build for one is
592
+ a page that already exists, without the enforcement.
490
593
  - Secret keys (`sk_`) must never appear in browser code; app keys (`pk_`)
491
- are public and domain-locked.
594
+ are public and domain-locked. A secret key can be SCOPED in the
595
+ dashboard's Keys room to particular collections, and to read-only or
596
+ read-and-update; a call outside that scope answers 403 `scope_denied`.
597
+ Read that error as deliberate — the human narrowed the key on purpose.
598
+ Name the collection and access your code needs and ask them to mint a
599
+ replacement key that includes it — scopes are fixed when a key is
600
+ created; never reach for a broader key than the job needs.
492
601
 
493
602
  ## Return values & shapes (get these exactly right)
494
603
 
@@ -500,6 +609,11 @@ rule. The specifics:
500
609
  synchronously from `g.collection(name)`; if you call that at module
501
610
  load, it can blank your whole app with no browser-console error. Name them
502
611
  right.
612
+ One more refusal, for collection AND field names alike: anything
613
+ JavaScript itself owns — `constructor`, `toString`, `__proto__`,
614
+ `hasOwnProperty` and their kin — is refused the same way a reserved
615
+ server-managed field is. A plain object inherits those names, so a
616
+ seal must never carry one.
503
617
  - The signed-in user: `await g.auth.currentUser()` →
504
618
  `{ authenticated: true, userId, email }` or `{ authenticated: false }`. The
505
619
  id field is **`userId`, not `id`** — `user.id` is `undefined`, and feeding
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.4.6",
4
- "description": "Gemmein SDK \u2014 passwordless auth, safe storage, and Stripe-driven record flips for AI-built apps. Small enough that one prompt teaches the whole API.",
3
+ "version": "0.4.7",
4
+ "description": "Gemmein SDK passwordless auth, safe storage, and Stripe-driven record flips for AI-built apps. Small enough that one prompt teaches the whole API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",