SDK reference
The complete surface of @gemmein/sdk — every method,
signature, and return shape. This exact reference also ships inside the package as
REFERENCE.md.
Setup
import { gemmein, gemmeinServer, GemmeinError } from "@gemmein/sdk"
const g = gemmein("pk_...") // browser: public key, domain-locked, safe to ship
const srv = gemmeinServer("sk_...") // server only — never in the browser (the SDK throws)
gemmein() accepts options
{ apiUrl?, tokenStore? }; gemmeinServer() takes
{ apiUrl? } only.
gemmein() throws missing_app_key/invalid_app_key if
the key is absent or an sk_.
Auth — g.auth
| Method | Signature | Returns |
|---|---|---|
sendEmailCode | (email) | Promise<void> — emails a sign-in code |
verifyEmailCode | ({ email, code }) | Promise<AuthSession> |
currentUser | () | Promise<CurrentUser> — never throws for session state |
logout | () | Promise<void> — revokes the server session |
type AuthSession = { token, expiresAt, user: { id, email } }
type CurrentUser = { authenticated: true, userId, email } // note: userId, NOT id
| { authenticated: false }
authenticated: false is deliberately silent about why — signed out,
suspended by the owner, and erased all read the same, so a moderated user's state never
leaks to the client. One signed-out screen covers all three.
Account — g.account
| Method | Signature | Returns |
|---|---|---|
delete | () | Promise<unknown> — erases the signed-in user, clears the stored token |
The “delete my account” screen. Every app it applies to needs one (GDPR right to erasure; Apple 5.1.1(v) for apps with account creation). Server-side it's the full cascade — sessions revoked, records and files deleted, subscription row removed. Irreversible: put a real confirm in front of it.
Data — g.collection<T>(name)
| Method | Signature | Returns |
|---|---|---|
create | (data, { key?, for?, published? }?) | Promise<GemmeinRecord> |
list | ({ limit?, sort?, where?, cursor?, search?, expand? }?) | Promise<ListResult> |
get | (id, { expand? }?) | Promise<GemmeinRecord> |
update | (id, data | atomicOps, { ifVersion?, published? }?) | Promise<GemmeinRecord> |
delete | (id) | Promise<void> |
upload | (file, { name? }?) | Promise<{ id, url, contentType, sizeBytes }> |
type GemmeinRecord<T> = {
id: string
data: T // YOUR fields live here — record.data.title, never record.title
createdAt: string
updatedAt: string
ownerUserId: string | null // server-set; null for app-owned (receipts, dashboard-created)
version: number // +1 per update; pass as { ifVersion } to guard concurrent edits
published: boolean // top-level; only meaningful on public rules
key?: string // the create-if-absent key, when one was used
audienceUserId?: string // recipient on addressed/direct (server-stamped)
expand?: Record<string, GemmeinRecord | null> // filled by { expand: [...] }
existing?: true // present when a keyed create returned YOUR existing record
}
type ListResult<T> = { records: GemmeinRecord<T>[], cursor?, hasMore }
sort: "newest" | "oldest" | "updated".
where: exact-match on data fields. expand: up to 3 link fields,
only on community/shared/direct. Atomic counters go
in value position: update(id, { stock: { decrement: 1, floor: 0 } }).
Payments
| Method | Signature | Returns |
|---|---|---|
g.subscriptions.mine | () | Promise<{ plan, status } | null> |
g.subscriptions.checkout | (plan?) | Promise<{ url, plan }> — navigates the browser to Stripe |
g.payments.buy | (product, { item? }?) | Promise<{ url, product, item? }> — navigates the browser to Stripe |
Server — gemmeinServer(sk)
| Method | Signature | Notes |
|---|---|---|
collection(name).get / .list / .update | as above | Scoped per collection in the dashboard — no creates, no deletes, no auth access |
testSession | (email) | Promise<{ token, expiresAt, user }> — dev environments only; sk_live throws test_session_forbidden_live. For the reaffirm harness. |
Keys & environments
| Prefix | Environment | Where it lives |
|---|---|---|
pk_test_... | Development | Frontend code — safe to expose |
pk_live_... | Production | Frontend code — safe to expose |
sk_dev_... / sk_live_... | Dev / Prod | Server env vars only |
Environments are fully isolated: different data, different users, different collections. Errors: see the full code table. Management (collections, domains, keys, payments config, logs) is dashboard-only — deliberately not in the SDK, so management credentials can never leak from app code.
Register your domain before you deploy. Requests are
checked against the app's allowed origins, and before go-live the only allowed origin
is localhost — so the first push to a preview URL fails every call with
403 origin_not_allowed, even though the same code worked locally a minute
earlier. Add it first, in the dashboard under Setup → “Reachable from your domain”.
The rule is stateful: a deployment that works before go-live can break after it if its
domain was never registered.
The numbers
Everything the server actually enforces, in one place. Nothing here is a soft target — each one is a real refusal with a real error code.
| What | Limit | On breach |
|---|---|---|
| Sign-in codes sent | 3 per email · 20 per IP · per 15 min | 429 with resetAt |
| Code attempts | 3 per email · 30 per IP · per 15 min | 429 with resetAt |
| Code lifetime | 10 minutes | ask for a new one |
| Session lifetime | 30 days | auth_expired once, then re-auth |
list() page size | 25 by default · 100 maximum | silently clamped — check hasMore |
| Record writes | 60 per minute, per app per IP | 429 with resetAt |
| Record reads | 150 per minute, per app per IP | 429 with resetAt |
In-app sends (addressed, direct) | 100 per hour, per sender | 429 with resetAt |
| File upload | 10 MB · images only | 413 file_too_large |
where filter | 5 fields, scalar values | invalid_filter |
The one that bites first: list() clamps
silently. No limit means 25 rows, not all of them — ask for 1000 and you get 100 with
no error. Neither case throws, so a list that looks short is usually pagination, not
missing data. Always read hasMore.