Changelog
What changed in each Gemmein release for apps built on Gemmein, newest first: each change as it was and as it is now, with the error codes it added or removed.
The same list is at changes.txt and changes.json, and in a terminal: npx gemmein changes --since <version>. The MCP server's changes tool gives your agent the same list.
Gemmein 0.23.0
· gemmein 0.23.0 · @gemmein/sdk 0.18.0 · @gemmein/mcp 0.16.0
After one browser approval the AI drives the build: sign-in by approval, declared collections, secrets saved for the AI, a Stripe key of 5 permissions plus 2 only if you sell plans (Gemmein sets up the billing portal), members on several devices, and a test kit so the AI proves the app in Development itself.
Pricing
- Changed
Was: Live apps paid one plan whose price moved in five pre-agreed steps from $50 up to $150 a month, set by the previous month's active people across the account (up to 10,000 people).
Now: Each Live app pays a flat price for the tier its founder picks: Launch $10 a month (up to 1,000 active people, 5 GB of storage), Live $40 (5,000 people, 50 GB) or Growing $100 (15,000 people, 150 GB); past 15,000 people, talk to Gemmein. Annual billing is 2 months off (save 17%): $100, $400 or $1,000 a year. Active people are a limit, never a price: past a tier's people the app keeps serving for the lesser of +10% or 7 days and the owner is emailed; then new people are refused at sign-in (403 people_limit_reached, "This app isn't taking new sign-ups right now.") until the app moves up a tier, while everyone already in keeps working. Nothing is ever charged automatically and no tier changes on its own. Building is free; downloads are fair use, not metered.
New error codes:
people_limit_reachedinvalid_planinterval_lockednot_live - Removed
Was: Holding the bill and the ceilings at a price step from the dashboard.
Now: There is no band to hold: the price is the tier the founder picks on Usage & billing, and the tier's people are where the ceilings stop.
Storage
- Changed
Was: On Live, storage grew with the app's active people (50 MB a person over a 100 GB base), plus any $5 blocks of 100 GB; uploads were refused past twice the ceiling.
Now: Storage is flat per tier (5 GB, 50 GB or 150 GB, never per person), plus 50 GB for each storage pack the founder adds ($5 a month, or $50 a year on annual billing). Past it the same grace as people applies — the lesser of +10% or 7 days, and an email — then new uploads are refused until files are deleted, a pack is added or the app moves up a tier. Files already stored stay. Development is capped at 1 GB, uploads refused past twice that.
Signing in the CLI
- Changed
Was: The CLI linked to the cloud app with a CLI key your human copied from the dashboard's Setup page and pasted.
Now:
npx gemmein loginprints a link and a short code; your human checks the code and presses Approve. No key is shown, copied or pasted. A Development approval lasts 24 hours.New error codes:
login_requiredslow_downpick_an_appwrong_appunknown_codeunknown_requestinvalid_machineinvalid_scopeinvalid_targetinvalid_actionnot_pendingnot_activerate_limitedtoo_many_pendingapproval_limitparent_endedapproval_keylocal_rail - Removed
Was: A CLI key shown on the Setup page and rotated on Secret keys.
Now: There is no CLI key. The CLI holds its own key in gemmein/.data/cloud.json after the browser approval; Revoke on the Approvals page ends it.
Error codes removed:
cli_key_unavailable
Live actions
- Changed
Was: Going live, promoting and
sync --liveasked for a typed confirmation word, andsync --liveneeded a one-hour sync key pasted from the dashboard.Now: Every Live action asks for its own browser approval, once, within 10 minutes, for exactly the plan shown item by item. There is no word to type and no key to paste. Approving any Live action also asks your human for the code Gemmein emails (once every 10 minutes) — except going live, where paying for the plan is the confirmation.
New error codes:
live_approval_requiredlive_approval_usedlive_approval_expiredlive_approval_wrong_actionapproval_plan_changedtoo_many_changesplan_requiredinvalid_relay - Added
Now: The dashboard's Approvals page lists the machines working now (Revoke ends one at once), the requests waiting (Cancel) and the history of what each approval was used for.
Collections
- Changed
Was: A collection's fields were picked up from the records written in Development and fixed when the app went live.
Now: Every community, shared and direct collection declares its fields in gemmein/collections/<name>.json. Writes to one with no fields are refused, a field not declared is refused, and go-live refuses until every one is declared. Nothing is guessed from test data.
New error codes:
collection_not_declaredundeclared_fieldundeclared_collections - Added
Now: The collection file holds every setting (rule, means, unlockedBy, publishing, openFields, directInitiation, fields). A key it doesn't know is refused by name, and sync applies the file's settings to Development.
New error codes:
unknown_collection_settinginvalid_fieldsinvalid_direct_initiation - Changed
Was: A collection's fields locked when the app went live; promote could only add new ones.
Now: A live collection only grows: a field may be loosened freely; tightening one shows how many records it would catch and asks, and only your human's yes applies it, to new writes and edits only (without it the promote is refused, tighten_needs_decision); a field is retired with "deprecated": true, never deleted; a field never changes kind; older records can show a declared "default".
New error codes:
field_deprecatedtighten_needs_decisionfield_type_changefield_missing_from_declaration
Going live
- Changed
Was: Going live on the dashboard showed a confirm pop-up after payment, and a collection with no fields yet stayed in Development.
Now: Going live is one screen at a time: pay, then progress, then a Live welcome card that keeps the result (what went live, anything that needs your human). Every declared collection goes live with exactly its declared fields, empty or not.
- Changed
Was: When a founder's Gemmein subscription ended or a payment failed, the whole account was suspended: every app refused every request with account_suspended, Development included.
Now: A failed Gemmein payment starts a 7-day grace: Live keeps working, the founder is emailed on days 0, 3, 5 and 6 and the dashboard counts down; on day 7, still unpaid, or at once when the plan ends, only Live pauses: app keys get 503 live_paused with "This app is temporarily unavailable. Please try again later.", server keys the founder's sentence; webhooks to Live are answered 200 and held, then run in order when Live is back (dropped after 30 days, logged, the founder told); Development keeps working, free, within its caps; nothing is deleted. Go-live and promote are refused while paused. A plan cancelled at period end is warned 7 days and 1 day before; paying again lifts the pause at once.
New error codes:
live_paused - Removed
Was: A suspended account answered every request, on every app, with account_suspended.
Now: No request is answered account_suspended. A paused Live answers 503 live_paused; Development keeps working.
Error codes removed:
account_suspended
Secrets
- Changed
Was: Secrets Gemmein made for relays and pipeline tools were shown once in the sync output for your human to copy to the app's host.
Now: Every secret Gemmein makes goes to gemmein/.data/secrets.json (kept out of git) with its variable name and who uses it; sync prints one line. You set them on the host yourself. A relay with no call_url gets no signing secret.
New error codes:
no_call_url - Added
Now: A provider's own signing secret (Stripe's whsec_…, or one an HMAC sender gives you) has a home: pipe it to
npx gemmein relays secret <name>(Development) or paste it on the relay's page in the dashboard.New error codes:
provider_secret_required
Server keys
- Added
Now:
npx gemmein keys create --name <n> --env development|live --access <collection>:<level>,…makes a server key with exactly that access on one browser approval; the key goes to the secrets file, never the screen.keyslists them,keys revokeends one.New error codes:
key_name_takenkey_not_foundinvalid_key_planno_environment - Added
Now:
gemmeinServer(sk, { expects })compares the key's access with what the app needs before the first call; too narrow or too broad throws key_scope_mismatch with err.missing or err.extra. whoami returns the key's access.New error codes:
key_scope_mismatch - Added
Now: A server key can hold the support Inbox:
--access inbox:read,inbox:reply(the dashboard's "Read your customers' support messages" and "Reply to your customers' support messages"). gemmeinServer(sk).listInboxThreads(), getInboxThread(id) and replyInboxThread(id, text) — GET /server/inbox/threads, GET /server/inbox/threads/:id, POST /server/inbox/threads/:id/reply — run the same guard and caps as the connector; a key's reply shows as sent by the key.New error codes:
inbox_auto_replyinbox_reply_capinbox_unreadableinbox_reply_limitedthread_not_foundinbox_unauthenticatedinbox_thread_limitedinbox_recipient_limited
Domains
- Added
Now:
npx gemmein domains add <domain>anddomains remove <domain>run on one browser approval; add prints the DNS records as one list and the one thing to do.New error codes:
domain_exists
Sync
- Changed
Was:
npx gemmein syncstopped at the cloud's change limit and had to be run again, and printed a long log.Now: Sync finishes in one run: it waits out the limit (30 changes a minute), syncs payments before collections, sends product files by hash and skips unchanged ones, and ends with a short summary and "What you need to do: nothing." or the one thing.
--verboseprints every line;--pruneremoves Development relays whose file is gone.
Allowed addresses
- Changed
Was: A browser call worked only from a listed domain or localhost; after go-live, from verified domains only, for every key. A builder's preview address (random and changing) was refused until it was listed.
Now: The Development app key works from any secure (https) address (a browser builder's preview such as Lovable, Bolt, v0 or Replit, a host's preview deploy, your own domain) and from localhost, before and after go-live, with nothing to list. The Live app key works only on verified domains, and
npx gemmein devkeeps localhost only. A page on a Development key that isn't https is refused with reasoninsecure, and every refusal carriesenvironment. Development sends 100 sign-in code emails a day per app (test members' codes don't count). Inside a frame,checkout(),buy()andmanage()open Stripe in a new tab. - Added
Now: Once an app is live, a page on one of its verified domains that still calls with the Development key is answered, and Live's Problems says so once a day per domain (
dev_key_on_live_site);npx gemmein domainsandnpx gemmein checkprint it. Switch the live build to the Live key (npx gemmein keys app).
Payments
- Changed
Was: The Stripe restricted key held five permissions, and an app selling plans also set up Stripe's customer portal by hand.
Now: The key holds 5 permissions, plus 2 only if you sell plans (Customer portal and Subscriptions, Write); none can pay out, refund, or read cards. Gemmein sets up the customer portal from your plans. A first plan on a five-permission key is refused before anything is made.
New error codes:
stripe_key_plan_rowsno_plansportal_unavailableportal_rate_limitedpayments_unreadable - Removed
Was: Pasting a customer portal link on the Payments page.
Now: Gemmein makes the portal;
g.subscriptions.manage()opens it for the member. There is nothing to paste.Error codes removed:
portal_not_set_upinvalid_portal_linkpayments_not_set_up - Changed
Was: Deleting a member's account left their Stripe subscriptions as they were.
Now: Deleting a member's account cancels every Stripe subscription they bought at the end of its period (never charged again, nothing refunded); one still owing is cancelled at once. If Gemmein can't read the app's payments at that moment, nothing is deleted and the call answers 503 erase_unavailable; try again.
New error codes:
erase_unavailable - Added
Now:
welcomeBonuson a plan or product adds credits once per member, only on their first paid payment in the app. No relay needed. - Added
Now: grant_credits takes
ref: once per person and ref, forever. - Added
Now: A plan-start email (gemmein/emails/plan-start.html) goes out when a paid plan or its free trial begins; each paid subscription invoice writes a receipt (in the app's receipts collection, when it keeps one); a Payment Link return carries a display-only ?paid=<item> marker for the thank-you screen.
Members
- Changed
Was: A person signing in on a second device ended their session on the first.
Now: Members stay signed in on up to 10 devices; an 11th sign-in signs out the oldest. g.auth.devices() lists them; signOutDevice(id) and signOutEverywhere() sign out. Signing out another device needs a recent sign-in, or 403 confirm_required and an emailed code.
New error codes:
device_not_foundinvalid_device - Changed
Was: g.auth.logout() revoked the person's one server session.
Now: g.auth.logout() ends this device's session only; the person's other devices stay signed in (signOutEverywhere() ends them all).
- Added
Now: A sign-in from a new device or country emails the member "New sign-in to <app>" (naming the country, never the address) with a single-use sign-out-everywhere link. Not on their first sign-in ever, read from the person's own record, so a person back after months away is still told; the owner can switch it off.
- Changed
Was: A server test session (testSession) signed the member out of their real browser.
Now: testSession signs in alongside: test sessions keep their own ten and never end a real one.
- Changed
Was: A suspended person was sent a sign-in code and refused after typing it.
Now: A suspended person is sent no code; the start answers the same 202 every address gets.
- Added
Now: currentUser() says why a stored session stopped working: ended "expired" or "revoked".
- Added
Now: g.auth.onSignedOut(listener) is the one place to route to sign-in: it fires when the session ends however it ended (reason "signed_out", "revoked" or "expired"), after the SDK has cleared its token, and returns a function that stops listening.
IP addresses
- Changed
Was: Logs rows kept the request's IP address, ordinary writes included: 90 days in Logs, then a 12-month archive with the address still in it; the founder saw it only on a person's activity.
Now: Gemmein records the full IP address only on security events, for members and the founder's own account: a sign-in code asked for or entered (allowed or refused), a session started, a sign-out (one device or everywhere), a rate-limited or refused sign-in attempt, a suspended person's attempt, a confirm code, a data export or deletion, a checkout a member starts, a browser approval asked for from the CLI, a secret key made or revoked. Ordinary reads and writes carry no address. It is read from the connection, never from a header the app sends.
- Added
Now: The founder sees each signed-in device's IP address and country on the person's page, and each security event's IP address in Logs, with a Copy button. The person's own devices() and the new-sign-in email name the country only. A CLI approval page shows the full IP address the request came from.
- Added
Now: An IP address is removed after 30 days (the event stays in Logs without it). The person's export carries the addresses still inside 30 days (sessions and signIns); deleting the person removes the addresses on their events, and the events stay.
- Added
Now: When a session ends, its device record (label, country and IP addresses) is deleted at once and leaves the list; the sign-in event in Logs keeps the device label and country.
Dashboard sign-in
- Changed
Was: When a founder signed in somewhere else, the other dashboard session ended with only "Your session ended".
Now: A founder has one dashboard session at a time: signing in somewhere else ends the other, and its sign-in page says "You were signed out because you signed in somewhere else."
Server SDK
- Added
Now: The server client's update takes { published, ifVersion }, and its get, list, create and update are typed: collection<Post>("feed").
Collection features
- Added
Now: An open field can be "one per person of a, b, c" (polls, RSVPs): each person holds one choice, set with open(id).choose(field, choice); the record carries a count per choice.
- Added
Now: The app is a party on a direct collection: your server writes { from: "app", for: personId } and a member replies { for: "app" }; your human answers from the dashboard.
- Changed
Was: "publishing": "owner approves" worked on community collections only.
Now: "owner approves" also works on shared collections (members-only moderated feeds). Waiting posts are approved or sent back inline on the Collections page.
New error codes:
not_moderatednot_waitingposts_unpublished
Email and domains
- Changed
Was: A domain that sends email needed five DNS records.
Now: It needs six: DMARC (v=DMARC1; p=none at _dmarc.<the sending host>) joins the others. If the main domain already publishes DMARC, that covers it and nothing is added.
- Added
Now: Designing the app's emails is a default build step;
npx gemmein checkand the go-live list warn when emails still use Gemmein's plain design. The purchase email for a credit pack says it's credits. - Changed
Was: The sign-in email and Gemmein's plain design (purchase, plan-start, notify) were always drawn light, in dark mode too.
Now: With no brandColour, backgroundColour or logoUrl in gemmein/emails/sign-in.json and no email accent in the console, the sign-in email and the plain design follow the reader's light or dark mode. With any of those set, they stay light, the background the colour or logo was made for.
Relays
- Changed
Was: Schedules ran every 15m, 30m, 1h, 6h, 12h or 1d.
Now: Schedules also run weekly: { "every": "1w", "on": "monday", "at": "09:00" }.
- Added
Now: On every receiver scheme the relay chooses whose secret it is: Gemmein makes it, or you paste the sender's. A pasted secret is plain ASCII, not a repeated pattern, and at least 24 characters (22 when it mixes upper- and lower-case letters and digits); a Standard Webhooks secret is whsec_…. A stripe receiver always uses Stripe's own.
- Added
Now: A relay's trigger kind can change freely under gemmein dev; once synced, the cloud refuses the change and names the two ways out (rename it, or
npx gemmein sync --prunein Development).New error codes:
trigger_kind_fixed - Changed
Was: An hmac_sha256_header receiver checked an HMAC-SHA256 of the body, or of <timestamp>.<body> with the timestamp in a header of its own. Senders that sign another way could not be verified: a timestamp inside the signature header (Paddle Billing, Slack), HMAC-SHA1 (Intercom), the URL with the body (Square), the URL with form fields (Twilio), a signature inside the body (Mailgun), or a public-key signature (SendGrid's ECDSA, Discord's Ed25519).
Now: The hmac_sha256_header scheme is now
signature: a signature recipe, described from the sender's own docs. A definition naming hmac_sha256_header is refused with 400 invalid_definition, naming the new scheme. algorithm is hmac-sha256 (the default), hmac-sha1, hmac-sha512, ed25519 or ecdsa-p256-sha256. The signature is in a header (signatureKey and separator for its parts) or in a body field (signatureField). The timestamp is in timestampHeader, timestampKey or timestampField. signedPayload is the signed text, built from {timestamp}, {url} and one of {body}, {params} or {token}. encoding is hex, base64 or base64url. Paddle Billing, Slack, GitHub, Intercom, Square, Twilio, Mailgun, SendGrid and Discord verify. Definitions without the new fields verify as before. - Added
Now: signedPayload rules, checked at save: the placeholders are {timestamp}, {url}, and ONE of {body}, {params} or {token}, which ends the text. Each appears once. Letters, digits and . : _ - go around them, at most 40 characters. A digit or {params} never comes right after {timestamp}. {timestamp} must appear when a timestamp is named and needs a source; {token} needs tokenField. A signature inside the body never signs {body} or {params}. timestampHeader, timestampKey and timestampField: one at most. signatureKey and timestampKey never share a name. The separator is , or ;. A break is refused with 400 invalid_definition, saying which and how to fix it. A recipe that saves but differs from how the sender signs answers 401 bad_signature on every event.
- Added
Now: A receiver whose recipe is ed25519 or ecdsa-p256-sha256 checks the sender's PUBLIC key. Paste it on the relay's page or pipe it to
npx gemmein relays secret <name>, as the sender shows it: hex, base64 or a PEM block. Gemmein never makes one: asking answers 400 provider_secret_required. A private key, a key of another kind, or anything that is not a key is refused with 400 invalid_input. A public key is never a secret: on an HMAC recipe a PEM or DER key is refused, and changing a recipe between a public key and a secret always clears the stored value in the same write, on save and on Promote; a paste racing such a change answers 409 version_conflict and stores nothing; an HMAC recipe that holds a key anyway refuses every event and raises the Problem relay_key_as_secret. A public-key change is on Logs by its fingerprint, never the key.New error codes:
provider_secret_requiredinvalid_inputrelay_key_as_secret - Added
Now: One rule for relays whose sender doesn't sign the event itself: a signature recipe that signs only a token ({token} ends signedPayload, as Mailgun signs) and the shared_token scheme. By default such a relay only writes records and calls your URL. Granting (grant_access, grant_credits, grant_plan, fulfil_product, start_run), taking away (revoke_access, revoke_plan, refund_product), emailing the person (email_person) and creating people (a map naming person_email) answer 400 invalid_definition, on the dashboard, the CLI, sync, gemmein dev and Promote (its dry run names it as not carried). person_id and person_token never create anyone. To let a relay act anyway, set trigger.trustUnsignedEvents: true on it: the save answers a one-line warning (the CLI and sync print it once), the relay's page shows it beside the setting, Logs records the change before and after, and Promote carries it with the warning in its dry run. The opt-in is refused on a sender that signs the event. A record a relay writes never fires a data_change relay. A stored relay that breaks the rule refuses every event with 401 bad_signature and raises the Problem relay_unsigned_event. An event that arrived unsigned never acts later: if the relay changed to act without the opt-in before it ran, it ends without running, and a replay of it answers 409 unsigned_event. With the opt-in on, a save that widens what a captured call buys answers the warning again. The Go live and Promote previews list each opted-in relay with its warning and each relay they won't copy. The RevenueCat and shared-token templates carry the setting, with the warning in their note.
New error codes:
invalid_definitionrelay_unsigned_eventunsigned_event - Added
Now: {url} in a recipe is the receiver URL exactly as the relay's page shows it, never the Host header. A call that adds anything after it, a query string included, answers 401 bad_signature. A receiver reads a form post (application/x-www-form-urlencoded) as its fields, so a map names them directly.
- Added
Now: A recipe whose signed text is not the raw body ({params} or {token}) accepts each signature once, still for 24 hours after the relay is deleted. A {token} recipe (Mailgun) does not authenticate the event: a captured signed call verifies once with any body within 5 minutes, so check such an event with the sender's API before granting. Senders that sign no timestamp (GitHub, Intercom, Square, Twilio) are deduped by event id, or by body within the UTC day.
- Added
Now: A signature header listing more than 8 signatures is refused with 400 too_many_signatures. A relay saved before the rename to signature answers 401 scheme_renamed until its definition is saved with "scheme": "signature".
New error codes:
too_many_signaturesscheme_renamed - Changed
Was: A signature timestamp was read as a number, so a sign, a decimal, an exponent, hex or a padded spelling could still verify.
Now: A timestamp verifies only as 1 to 13 digits; anything else never verifies.
- Changed
Was: The dashboard's shared-token receiver template said GoCardless, Lemon Squeezy and Paddle send a shared token.
Now: They sign an HMAC: GoCardless and Lemon Squeezy a plain hex HMAC of the body (header alone), Paddle Billing with timestampKey, signatureKey, separator and signedPayload.
AI tools
- Changed
Was: A transcription tool accepted any response_format when saved.
Now: An unsupported response_format is refused when the tool is saved, in gemmein dev and at sync alike, naming the formats that work.
New error codes:
unsupported_response_format - Added
Now: Under gemmein dev, your own pipeline can hand back files at a loopback http URL (the cloud still takes https only); the SDK's pipeline helper fails the run, and the hold returns, when the hand-back is refused.
- Changed
Was: A chat tool ran only on the three hosts (OpenAI, Anthropic and Google); any other model needed your own pipeline.
Now: A chat tool also runs on any endpoint that speaks the OpenAI Chat Completions format — provider
openai-compatible: your human adds a name, the base URL, the key and (optionally) the models on the AI page (OpenRouter, Mistral, Groq, DeepSeek, xAI, Together AI, Fireworks AI; Ollama undergemmein devthrough GEMMEIN_AI_URL_OPENAI_COMPATIBLE). Model ids are the endpoint's own (slashes allowed). The base URL is checked on every call — https only, public addresses only, redirects never followed — and a new base URL is saved only with its key pasted again. Text only unless the tool ticks image.New error codes:
invalid_base_urlendpoint_refused - Added
Now: Every AI answer passes at most 32 MB; past it the answer is cut where it stands and costs its ceiling.
Test kit (Development)
- Added
Now:
npx gemmein members add <name>makes test members at <name>@<tag>.gemmein.test;npx gemmein mailreads their sign-in codes and every email the app sends them, with headers and SPF/DKIM/DMARC. Development only; their mail never leaves Gemmein.New error codes:
test_kit_development_onlytest_members_fulltest_kit_cappednot_a_test_member - Added
Now: The founder's actions on test members from the CLI: approve, send-back, suspend, unsuspend, sign-out --everywhere.
New error codes:
changed_since_read - Added
Now:
npx gemmein pay --as <member> "<item>"pays through the app's real checkout with Stripe's test card and waits for the webhook;npx gemmein refund <payment>refunds it. They run only on a test-mode Stripe key and are refused on a live one. Two optional rows on a sandbox key serve them: Charges and Refunds (Write) and Checkout Sessions (Read).New error codes:
test_pay_live_key_refusedtest_pay_localtest_pay_unavailabletest_refund_refusedtest_refund_outcome_unknowntest_checkout_unknownpayment_not_foundunknown_itemstripe_not_connected
Hosted connector
- Added
Now: AI apps with no terminal — ChatGPT, Claude, Lovable, Replit, Bolt, v0 — add https://api.gemmein.com/mcp and build and run a Gemmein app from the chat. The ten tools of
npx -y @gemmein/mcpneed no account. One approval on the same pagenpx gemmein loginuses lets the connector build in Development: pull and sync the gemmein/ files (the files win, the diff shown first), or declare collections, plans, relays, AI tools and emails one at a time; test members, test mail and sandbox test payments. Live is never part of it: each Live change is approved on its own, with the code Gemmein emails — except going live, where paying for the plan is the confirmation. Reading Live is its own approval line, and every read is on Logs. Secret keys are shown once in the browser, never in the chat. The connection lasts 30 days and is revoked on Approvals.npx gemmein devserves the same /mcp on this computer.New error codes:
sign_in_requiredinvalid_tokeninsufficient_scopekey_in_browsertoken_in_urlhost_refusedorigin_refused - Added
Now: Six read-only Live tools on the live:read grant answer "how is my app doing?" without the dashboard: live_overview (people and revenue this month against last, errors in the last 24 hours, what waits on the founder, paused or not), live_errors (each code with counts, first and last seen, an example Logs row and its docs page), live_usage (meters against the plan, AI calls, credits used and given, emails sent), live_webhooks (relay runs, retries, each failure's reason), live_emails (sent, bounced, complained, deferred per template) and live_inbox (conversations, unanswered first; one in full). Each read is on Logs and changes nothing.
New error codes:
unknown_relaythread_not_found - Added
Now: inbox_reply answers a customer inside an existing Inbox conversation, from the app's support address, exactly as the founder's reply — its own approval line (live:inbox_reply, "Reply to your customers' messages"), the one standing Live action. Never after an automatic message (inbox_auto_reply), at most 3 AI replies in a row (inbox_reply_cap), never while the last message can't be read (inbox_unreadable), at most 60 an hour per connection and 300 per app (inbox_reply_limited). Only to senders whose email passes DMARC, as the receiving provider saw it (inbox_unauthenticated otherwise; you can reply to anyone yourself); at most 10 a day per conversation (inbox_thread_limited) and 5 an hour per address (inbox_recipient_limited); Live's side of the Inbox only. Each reply shows in the Inbox as sent by your AI.
New error codes:
inbox_auto_replyinbox_reply_capinbox_unreadableinbox_reply_limitedinbox_unauthenticatedinbox_thread_limitedinbox_recipient_limited
Errors
- Added
Now: A refusal in Development, from a secret key, the CLI or the MCP connector carries
docs, the code's own page:{ code, message, docs: "https://docs.gemmein.com/errors/<code>" }.GemmeinErrorhas.docs(JS and Swift). A request made with the Live app key is answered without it, so nothing a customer can read names Gemmein.
MCP server
- Changed
Was: search_docs returned passages of the packaged guide and reference by line; reference and guide returned whole documents.
Now: search_docs answers first with the docs site's sections that match, each a direct markdown link with its anchor (https://docs.gemmein.com/<page>.md#<section>). guide and reference take an optional topic and return only the matching sections; guide with no topic ends with every page's markdown link. explain_error names the code's page, https://docs.gemmein.com/errors/<code>. Offline: the section index ships with the server.
What changed
- Added
Now: This feed:
npx gemmein changes [--since <version>] [--json], the MCP toolchanges, and https://docs.gemmein.com/changes.txt (and .json). The guide opens with its version and what changed.
Founder sign-in
- Added
Now: A founder can turn on an authenticator app (TOTP: 6 digits, 30 seconds) in the dashboard's Settings › General, each person on an account for themselves. With it on, signing in to the dashboard asks for its code after the emailed code, and so does deleting a Live collection, a domain of a live app or the account, approving such a delete for an AI, and turning it off. Ten single-use recovery codes are shown once and work in its place. Nothing in Development asks; prices, plans, keys, Stripe, going live and promote never do. Ten wrong codes in a row slow the next try (1 minute up to a day) and email the founder — never a lockout. For an AI: a Live delete you ask for waits on your human's Approve as before — that page may now ask them for the code.
New error codes:
app_code_requiredapp_code_invalidapp_code_sign_in_expiredapp_code_already_onapp_code_not_pendingapp_code_not_onapp_code_wait
No longer true
Things that were true of Gemmein once and are not now.
Was: Gemmein runs nothing for you: it has no compute of its own, and a relay only passes events along.
Now: Gemmein runs its own AI tools and relays. A relay receives the event, maps fields and carries out Gemmein's own actions; it never runs code you write or reshapes data with custom logic. An external pipeline tool and your own server code run on your own host.
Was: There are no native mobile SDKs.
Now: Native mobile SDKs exist: @gemmein/sdk/expo (Expo and React Native) and GemmeinSwift (Apple platforms).
Was: Gemmein charges for each active person.
Now: Each Live app pays a flat price for the tier its founder picks; active people are a limit, never a price, and there is never a per-person invoice line.
Was: Every payment lands in a single Stripe account, and payments are Stripe-only.
Now: Money always lands whole in the founder's own payment provider account: Stripe is built in, others come through a relay. Gemmein never holds it.
Was: The free (default) plan can't carry a credit allowance.
Now: The free (default) plan may carry credits.perPeriod (granted once per person per calendar month, UTC, at their first spend) and/or credits.once, a welcome allowance for a person who never held a paid plan.
Was: People can't upload their own audio for transcription.
Now: A transcribe tool's file input takes audio the person uploads with g.ai.upload(tool, file) (mp3, m4a, wav, webm, ogg, flac; 25 MB, 14 MB on Google), a file they already own, or an https URL.
Was: Signing in to your own app as its owner lifts your role, so the app can show you everyone's records.
Now: In the app the owner is an ordinary customer and reads only what the rule gives them. The Gemmein dashboard is the admin view; an in-app admin screen goes through your own server with a read-only secret key.
Was: sends_disabled means the owner switched in-app sending off.
Now: sends_disabled means direct messages are off for this collection; addressed records are sent from the dashboard or a relay.
Was: A secret key can only read and update records; it can't create or delete them.
Now: A secret key gets one of three levels per collection (reads only; reads and writes; or reads, writes and deletes), or is a full key for every collection. A key that writes creates and updates records under any rule; name whose record it is with { for } or { from }.
Was: You can paste your own Stripe Payment Link and signing secret instead of connecting a key.
Now: Selling through Stripe needs the restricted-key connection on the Payments page; Gemmein makes each Payment Link.
Was: Live storage is 10 GB per app.
Now: Live storage is flat per tier (5 GB, 50 GB or 150 GB) plus 50 GB per storage pack; Development is capped at 1 GB.
Was: The CLI links to the cloud with a CLI key your human copies from the Setup page.
Now:
npx gemmein loginprints a link and a code; your human presses Approve in the browser. No key is ever shown, copied or pasted.Was: Live actions wait for you to type a confirmation.
Now: Each Live action is approved in the browser, once, for exactly the plan shown; there is nothing to type.
Was: Gemmein picks up a collection's fields from the records you write in Development.
Now: Every community, shared and direct collection declares its fields in gemmein/collections/<name>.json; nothing is guessed from test data.
Was: Signing in on a second device signs the person out of the first.
Now: Members stay signed in on up to 10 devices, each listed and signed out on its own; founders keep one dashboard session.
Was: A plan-selling app needs its customer portal set up in Stripe and the link pasted on Payments.
Now: Gemmein sets up Stripe's customer portal from your plans; g.subscriptions.manage() opens it. There is nothing to paste.
Was: A chat AI tool runs only on the three hosts: OpenAI, Anthropic and Google.
Now: A chat AI tool runs on OpenAI, Anthropic, Google or any endpoint that speaks the OpenAI Chat Completions format (provider openai-compatible: a base URL, a key and the endpoint's own model ids).
Was: Going live drops localhost and every unverified domain for every key; before go-live a page works only on a listed domain or localhost.
Now: The rule is per key: the Development app key works from any secure (https) address and from localhost, before and after go-live, with nothing to list; the Live app key works only on verified domains.
Was: Live apps pay one plan whose price steps from $50 up to $150 a month with the previous month's active people.
Now: Each Live app pays a flat price for its tier: Launch $10, Live $40 or Growing $100 a month (or $100, $400, $1,000 a year); past 15,000 people, talk to Gemmein.
Thanks for telling us.
Thanks for telling us.
Last updated