reference

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_reached invalid_plan interval_locked not_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

Live actions

  • Changed

    Was: Going live, promoting and sync --live asked for a typed confirmation word, and sync --live needed 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_required live_approval_used live_approval_expired live_approval_wrong_action approval_plan_changed too_many_changes plan_required invalid_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_declared undeclared_field undeclared_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_setting invalid_fields invalid_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_deprecated tighten_needs_decision field_type_change field_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. keys lists them, keys revoke ends one.

    New error codes: key_name_taken key_not_found invalid_key_plan no_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_reply inbox_reply_cap inbox_unreadable inbox_reply_limited thread_not_found inbox_unauthenticated inbox_thread_limited inbox_recipient_limited

Domains

  • Added

    Now: npx gemmein domains add <domain> and domains 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 sync stopped 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. --verbose prints every line; --prune removes 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 dev keeps localhost only. A page on a Development key that isn't https is refused with reason insecure, and every refusal carries environment. Development sends 100 sign-in code emails a day per app (test members' codes don't count). Inside a frame, checkout(), buy() and manage() 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 domains and npx gemmein check print 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_rows no_plans portal_unavailable portal_rate_limited payments_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_up invalid_portal_link payments_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: welcomeBonus on 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_found invalid_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_moderated not_waiting posts_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 check and 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 --prune in 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_required invalid_input relay_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_definition relay_unsigned_event unsigned_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_signatures scheme_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 under gemmein dev through 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_url endpoint_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)

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/mcp need no account. One approval on the same page npx gemmein login uses 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 dev serves the same /mcp on this computer.

    New error codes: sign_in_required invalid_token insufficient_scope key_in_browser token_in_url host_refused origin_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_relay thread_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_reply inbox_reply_cap inbox_unreadable inbox_reply_limited inbox_unauthenticated inbox_thread_limited inbox_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>" }. GemmeinError has .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 tool changes, 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_required app_code_invalid app_code_sign_in_expired app_code_already_on app_code_not_pending app_code_not_on app_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 login prints 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.

Was this page helpful?

Last updated