reference

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

MethodSignatureReturns
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

MethodSignatureReturns
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)

MethodSignatureReturns
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

MethodSignatureReturns
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)

MethodSignatureNotes
collection(name).get / .list / .updateas aboveScoped 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

PrefixEnvironmentWhere it lives
pk_test_...DevelopmentFrontend code — safe to expose
pk_live_...ProductionFrontend code — safe to expose
sk_dev_... / sk_live_...Dev / ProdServer 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.

WhatLimitOn breach
Sign-in codes sent3 per email · 20 per IP · per 15 min429 with resetAt
Code attempts3 per email · 30 per IP · per 15 min429 with resetAt
Code lifetime10 minutesask for a new one
Session lifetime30 daysauth_expired once, then re-auth
list() page size25 by default · 100 maximumsilently clamped — check hasMore
Record writes60 per minute, per app per IP429 with resetAt
Record reads150 per minute, per app per IP429 with resetAt
In-app sends (addressed, direct)100 per hour, per sender429 with resetAt
File upload10 MB · images only413 file_too_large
where filter5 fields, scalar valuesinvalid_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.