Every change Wellknown observed on this MCP server, newest first, with what it was before and what it became. Tool-surface changes carry the definition diff. Nothing here is edited after the fact.
Added "neblla_alerts_read", "neblla_assets", "neblla_auth_secret_read" and 46 more; removed "neblla_set_communication_read"; changed the definition of "neblla_alerts", "neblla_archive_app", "neblla_auth_secret" and 114 more (118 tools before, 166 now)
before
—after
AlertsThe app's alerts, as its panel shows them: what needs the developer now (store full with its 7-day countdown, their own database not answering, payments waiting for it, a data migration paused for the quota, quota out or in its 10 % margin, users saving in a loop, formal legal notices on community entries, the community put on hold by Neblla, data erasure requests, reviews withoutDismisses ananswer, a "Pasar a pro" batch that failed, automatic recharges done or failed) and the history (90 days). Eachalertsays in plain words what happens (`text.what`),what the users notice (`text.users`) and what to do or who has to look at it (`text.todo`); `urgent` ones go by email to the owner by default. action "list" { lang?, topic?, history? } → { attention, history, counts }; "dismiss" { id } (or { all: true }) marks seen; "settings" → per alert type who gets it (`to`: "owner" orsetsteamwhichemails,alertsfromareneblla_team)sent and where(`via`: "email","slack", "discord"; the panel always has it), and whether a Slack / Discord webhook is set; "set_settings" { types?: { [type]: { to?, via? } | null }, slack?: url | null, discord?: url | null } (in the act); "test" { via, to? }or sends atest message. Alerts that already had their own email (store full, legal notice, community hold, automatic recharge) still email the ownertest.whateverMore:isneblla_get_llmschosen.tools/alerts
{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": dismiss {id}; test {via, to?}","enum":["list","dismiss","settings","set_settings","test"],"type":"string"} ⟨60 unchanged words⟩ de, it, pt, ja, ko, zh, ar); English by default","type":"string"},"slack":{ ⟨36 unchanged words⟩ database, plan, moderation, community, inbox, gallery, publish)","type":"string"},"types":{"additionalProperties":{},"description":"set_settings: { store_full: { to: ⟨5 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
—after
Archive appArchiveArchives an app nobody uses: its datais bundledgoesontoto the CDN andtheits active records arecleared from Neblla's database. It stays in the gallery and iscleared.restoredRestoresautomaticallyitself the next time anyone opensit (first open is slower). Neblla also does this on its own for free apps with no developer or user activity for 30 days; apps with antheactive
before
—after
Auth secretThe app's secret login key, for apps that already have their own users ("Tu propio login" in neblla_get_llms({ section: "identidad" })). Their server calls POST https://neblla.com/api/apps/<appId>/auth/token with Authorization: Bearer <key> and { externalId, email?, emailVerified?, name? }, gets a one-use token valid one minute, and the front calls neblla.auth.login({ token }). action "create" makes the key (refused if one exists), "rotate" replaces it (the old one stops working at once), "status" says whether one existsCreates,"revoke" removes it. The key is returned ONLY by create and rotate: tell the developer to put it in their server's environment,rotatesneverorinrevokes the app'scode. Whoever has it can sign in as any user of this app — treatsecretitloginlike
before
—after
BackupsThe backups of the data Neblla keeps for an app: the copies of the last 7 days (30 with Pro; `keepDays` says which), taken from Neblla's daily encrypted copy of its databases, and restoring one whole. Actions: "list" (enabled: whether Neblla keeps copies at all; envs.pro / envs.pre: byodb, usesProData, the last restore; days: [{ date, createdAt, pro, pre }] with how many rows of each kind the copy holds — players, saves, tables, inbox, community, chat, rooms, purchases, customers; null with the developer's own database, which Neblla does not copy); "restore" (date, env "pro" by default or "pre": EVERYTHING of that environment goes back to that day — saves, tables, inbox, community, chat, rooms; players who are still there getRestorestheirarowbackup ofthat day, who arrived since keeps theirs withannoapp's data,who was erased since does not come back; purchases and Stripe customers are never changed, only missing ones come back. Irreversible: confirm with
before
—after
Ban settingsReadSets themessagemessages a banneduser sees in this app,andthe onea terminated usersees, or change them with `bannedMessage` / `terminatedMessage` ("" puts Neblla's default back). A banned user reads it above a form to write to you; a terminated one has no form, so itsseemessageinshouldthenotapp.pointMore:at
before
—after
BillingWhat the developer pays Neblla: theProsubscription of a PROJECT (€10 a month per project, or $10 outside Europe, taxes included; every app of a Pro project shares its own pool of 2000 credits a dayandstops drawing from the account's free pool) and one-offquota packs(they go to the pool the project draws from now: theaccount's for a free project, the project's own while it is Pro). Name the project with projectId, or name an app (appId) and it is that app's project (neblla_projects "list" says which apps are where; most accounts only have Explorer). "pro_checkout" and "quota_pack_checkout" return a URL the DEVELOPER opens to pay — an agent cannot complete a payment, and nothing is granted until Stripe confirms it, so tell them the upgrade lands a few seconds after they finish. "cancel_pro" stops Pro from renewing: it stays until the end of the period already paid (answers endsAt)checkouts andthen the project is Free; nothing is refunded. Only call it whenswitching thedeveloper has asked for it in as many words. "resume_pro" takes a pending cancellation back while that period lasts: Pro renews again
before
—after
BookingBookings (business template), the administration side: action "createSlot" { set, start, end?, capacity, title?, repeat? } — repeat is a calendar rule (the usual RFC 5545 RRULE): { freq: "daily"|"weekly"|"monthly"|"yearly", interval?, byDay? (["MO","WE"]; monthly "2TU" = second Tuesday, "-1FR" = last Friday), byMonthDay? (15, -1 = last day), byMonth?, until? (a date: that whole day) or count?, except? (dates left out), timeZone? (IANA: keeps the local hour when the clocks change; left out, the zone of its site or its hours, then its professional's hours, else UTC) } or an RRULE string ("FREQ=WEEKLY;BYDAY=TU,TH;COUNT=20"); its cases are slots sharing a series id, made a year ahead and the next ones as time goes by (no end needed), all or none. "updateSlot" { set, slot, scope?, start?, end?, capacity?, title?, repeat? } and "deleteSlot" { set, slot, scope? }: scope "one" (default; that case alone), "following" (this and the next) or "all" (every case not started yet)Creates,as in a calendar; bookings of cases that stay are keptchanges,a case that goescancelsits bookings with their creditsormoney back. "series" { set, series } (its rule), "slots" { set, from?, to? } (places left), "list" { set, slot? } (with who), "cancel" { set, booking, refund? } (by default, refunded only within the set deadline), "move" { set, booking, toSlot }, "cancelSession" { set, slot, reason? } (the whole session, everyone gets their credits or money back; it stays in the agenda as cancelled), "times" { set, from?, to?, site?, staff?, step? } (the free times of a service booked at a time). "series" { set } without an id lists the set's repetitions (their ids sell a standing place: business.standing { <productId>: { set, series } }, each purchase books its buyer in every case of the period paid; several ids offer several timetables in one product, and the client picks one). "changeStanding" { set, user, productId, series, when? }
before
—after
BroadcastWriteSends a message toALLall the users of an app, orto the membersof one group(optionally only one role in it): the message lands in each one's inbox as a message from the developer (neblla.inbox; a connected user gets it live,inbox.on('updated') with action "created"). It is paid per recipient from the app's daily quota, like any message (msgFixedOut + the bytes of messageanddata, kind newCommunication). action "estimate" { message, data?, group?, role? } says beforehand what it will cost: recipients, credits, what is left today (available), percentOfDay and how many that reaches (reachable) — ALWAYS show this to the developer and get their OK before "send". "send" (same fields) answers at once and sends in the background in batches; it never goes past the quota: when the day's quota runs out it stops (status "stopped_quota", with resumesAt), raises the app alert broadcast_paused and goes on by itself from there after the 00:00 UTC reset; "resume" { id } goes on sooner once there is quotaresumes
before
—after
BusinessThe places and people of a business (C200), the administration side: what its bookings use. kind "sites" (a place: { name, timeZone?, address?, hours? }), "resources" (a group of identical units: { name, site?, count (how many: 30 tables, 200 spaces; 1), capacity (how many fit in one; 1), hours? }) and "staff" (the professionals: { name, email?, sites? ([ids] where they work), hours? }). hours: { timeZone?, week: { MO: ["09:00-14:00", "16:00-20:00"], … } (a day left out is closed; a professional's range can say where: { from, to, site }), closed?: ["2026-12-25"] }; none: always open. action "list" { kind }, "save" { kind, id?, …fields } (makes one, or changes the one with that id: only the fields given), "remove" { kind, id } (refused with in_use while a session to come or a service uses it). "accounts" { from?, to? } (this month by default): per professional, site and resource, the sessions, minutes and people, to pay them outside Neblla; andChanges thenotes written by hand with their total per who. "agenda" { from, to } (at most 62 days): every service's sessions in that time, each with its set, who booked, itsplacesand its state (confirmed,proposed, cancelled), as a calendar shows them. "note" { who ("staff:<id>", "site:<id>" or a name), amount (cents: what the business owes them, negative when it paid), date?, note? }people,
before
—after
ByodbBring-your-own-database: move where this appMigratesstorestheitsapp's users' data(saves, rooms, community, tables; the registry of users, purchases and friendships follows) from Neblla's default store (community included, so its entries can then be queried with their own SQL)to the developer's own database, orback. Actions: "status" (current engine + migration progress: progress {phase, processed, total, left}, cost {reads, writes, bytes, credits} spent so far, pausedForQuota + resumesAt, lastMigration); "validate" (engine + config, tests the connection, changes nothing); "migrate" (validates again, then starts a 3-phase background migration of ALL user data — confirm with the developer first); "to_neblla" (leave their database: the same migrationback to Neblla'sstore, ALL of it comes back, players, purchases and records included — confirm first; if their database does not answer it fails with store_unavailable and canStartFresh, and "to_neblla" with fresh:true then starts the app FROM SCRATCH on Neblla's store, discarding what waits for their database: only with the developer's explicit go-ahead); "cancel" (only before the last phase). Every migration of theirs, there and back, is paid from the app's quota (reads, writes and bytes moved, kind "migration"): when the quota runs out the copy pauses and goes on by itself after the daily reset (or a pack); the final short lock always ends and is charged. Engines and config: mongo {uri, dbName}, turso {url, authToken}, postgres {uri}, mysql {uri}. Credentials are stored encrypted and never returned. env "pre" works on the app's pre: by default it follows the app's database (and moves with it); migrating it gives pre a database of its own, and "migrate" with
before
—after
ByosThe developer’s server on THEIR Cloudflare account, deployed by Neblla (serverMode "own"; Cloudflare bills them, Neblla only the relay). The Cloudflare token is the ACCOUNT'sDeploys orthe PROJECT's connection, the topic "server" of Conexiones (neblla_projects "cloudflare_setup"), shared by their apps; it is pasted in the panel, never in the chat. action "set" { provider: "cloudflare" }: uses the token already connected (it needs Workers Scripts: Edit, and the account’s workers.dev subdomain on); cloudflare_not_connected with the panel's link when there is none. Then switches the app to "own" and deploys the box —removes thesameapp's room.jsrunner as Neblla’s cloud; saving a server/ file redeploys it. "status": the account,on thebox and its last deploy (neverdeveloper'stheCloudflaretoken).
before
—after
Community casesFormal notices filed about the community entries of this app from OUTSIDE it, with no account — the route a rights holder or anyone who spots illegal content uses. Filing one suspends the entry immediately and opensCloses acase here. Actions: "list" (optional status open|countered|upheld|dismissed) — open cases first, since those are theformalonesnoticeneedingabout ahuman; "get" (caseId) — the full claim; "close" (caseId, outcome upheld|dismissed) — decides the CASEcommunityonly.entryClosingasdoesupheld
before
—after
Consume purchaseConsumeConsumes units of a user's purchased productfrom the backoffice, forsupport fixes. It only SPENDS units andsupport; movesNO money — it is not a refund. Refunds are made in Stripe, from the dashboard that neblla_payments_account dashboard_link opens. Never call this for a customer who says they paid and did not receive something — it would take away what little theynohave.money.AtomicMore:fails with insufficient_amount if the user has fewer units. The user's app gets a live `purchase`neblla_get_llmsevent.tools/consume_purchase
before
—after
Copy app configCopyCopies the behavioural settings(features, rooms policy, matchmaking, inbox, p2p, serverMode, permissions, theme, connections, ai…) from oneofyour apps (what it is being edited to: its pre with its draft) into another's draft; the users get them with neblla_promote "publish". Name, files, domains, slug, gallery and billing are never copied. Overwrites those settings on the target — confirm first. Anone appfrom before the modules (features with the old switches: multiplayer, login…) gives the modules they mean. Returns `copied` (the fields copied) and `skipped` ([{ field, error,tomessageanother.}]More:what the target refused, e.g. a setting in an old shape; set it by hand withneblla_get_llms
before
—after
Copy community entriesCopyCopies community entries from one ofyour apps to another, server-side, payload included: each is published inthetarget AS YOURSELF (like neblla_publish_community_entry: users are per app, so the original authors cannot come along; their names stay in the source). Oldest first, and an entry whose parent is copied too keeps pointing at it (comments, versions, collections). Votes, favorites, reviews and reports stay with the source. The target needs the community module, and its rules apply (contentTypes, maxPayloadBytes). All the source's published entries (up to 500), or only `entryIds`. Returns `copied` ([{developer'sfrom,apps to}]) and `failed` ([{another.id,More:errorneblla_get_llms
before
—after
Copy filesCopyCopies files from one ofyour apps to another, server-side: whatthesource is being edited to (its pre with its draft) lands in the target's draft; the users get them with neblla_promote "publish". All the source's files, or only `paths`. A path the target already has is overwritten — confirm with the developer first. The target's file cap still applies (30 on free, 150 on Pro): when it is reached the copy stops and says what was left. An appdeveloper'slinkedapps toits own repo (neblla_git) takes them as one commit. Returns `copied` (paths), `missing` (paths whose content the sourceanother.lostMore:upload them again) and, if any,neblla_get_llms`notCopied`.
before
—after
Create appCreate a new Neblla app. Returns the app ID (appId) to use in subsequent file upload operations. AI BUILDS IT IN MINUTES, NEBLLA KEEPS IT ALIVE FOR YEARS: you write the app, and Neblla is the backend and the back office of their game, app or shop, everything that would otherwise need a server of their own — accounts that work across all of their apps, saved per-user data, payments, content their users publish, AI, and real-time rooms when more than one device is involved. It is NOT only for multiplayer games: a single-player game that saves progress, a tool behind a login, a gallery of user content and a shop where the developer sells are all squarely the use case. SHARED DOCUMENTS: room.data starts with a 10 KB anti-abuse limit the developer can change or turn off, but a document two people edit lives better in community entries (64 KB by default, one entry per person so there is no write conflict) with the room carrying only the notice that something changed — recipe in neblla_get_llms({ section: "comunidad" }). WHEN TO REACH FOR THIS — any ONE of these is enough, they are not a checklist: the app needs user accounts or sign-in; data that survives a reload; to charge for something; content its own users publish and browse; or more than one device at a time. A single-player game that saves progress, a tool behind a login, a gallery of user content, a shop where the developer sells,Creates an appof many sellers each paid on their own Stripe — all fit. Users reselling to EACH OTHER what they bought does not. Only if the page runs entirely in one tab with nothing to save and nobody to sign in is a plain index.html the better answer — say so then instead of creating an app. NEBLLA DOES NOT HAVE TO HOST THE APP
before
—after
Create hookMakeMakes an incoming webhook: a URL a third party(Twitch, a payment provider, n8n…)callswith a JSON body, and Neblla hands that bodytoa room — its server file hears neblla.room.on("hook", ({ name, data }) => …) on the Fogs — or, with to: "user", to the user named in the call (?user=<their id in the app>), or, with to: "inbox", leaves it as a notice in the app's Buzón (People › Inbox) even when nobody is connected: the hook's name as sender, a readable line of the body (its message/text/title, else its first values) and the body as data; it cannot be answered, and past the inbox cap per sender the oldest notice goes. Needs the inbox module. To get an event of the app itself there (a purchase, a refund…), make an API secret with this URL as base and point an outgoing webhook at it. The URL carries a token shown only now; making it again with the same name gives a new token (the old URL stops working). Up to 20 per app; 64 KB per call; 60 callsreach aminute per hook. Answers: 200 { delivered }, 404 hook_not_found, 409 room_not_live / user_not_online / feature_disabledroom.(to
before
—after
Create productCreateCreates apurchasableproductfor a Neblla app. type must be "one_time" (single purchase, fixed price), "consumable" (quantity selector, price per unit) or "subscription" (renews every month or year by itself: price per period, interval, optional trialDays; the user manages it with neblla.shop.manage). name should be a multi-language object e.g. { en: "Gold Coins", es: "Monedas de Oro" }. price (one_time, subscription) and pricePerUnit (consumable)toaresell incents (e.g. 499 = $4.99). A product isanentitlement inside theapp— a permission or a counter — or, with `shipping`, a PHYSICAL product (one_time or consumable):prints of a photo, mugs, T-shirts, a printed fanzine. shipping: { zones: [{ name, countries: ["ES"], amount }] } (postage in cents, 0 free; a country inonezone): the checkout asks for the address in the zone of the buyer's country (neblla.shop.buy(id, n, { country: "ES" }); with one zone it needs none; several and no country answers country_required with the zones, for the app to ask) and charges its postage; each payment becomes an ORDER (neblla_orders: paid → shipped with carrier and tracking → delivered, or returned), the buyer hears each step in their inbox, and the EU's 14 days to withdraw show on each delivered order. Labels, warehouses and carts of many articles are Shopify's: say so when a shop needs them. WHO IS PAID: by default the app's payout account (the developer, or the project's own Stripe). An app with MANY SELLERS (the clubs of a padel app, the artists of a gallery, the authors of a shop of templates, the teachers of a platform) gives a product a `seller` ("site:<id>", a site of the business; "user:<id>", a user of the app): it is charged on THAT seller's own Stripe account, which pays its refunds and disputes, and the developer takes their commission (neblla_update_app business.sellers.fee) — see neblla_business "sellers". Until the seller's account is active the product is listed as waiting and its checkout answers seller_waiting. Reselling between users is not there. WHAT IS SOLD IS OFTEN NOT A FILE: `content` gives a one_time product or a subscription private links or texts (a Canva template «Abrir en Canva», a hidden course video, a game code, a Discord invite), read only by who holds it with neblla.media.mine(); files are unlocked with neblla_media "link". UNITS FOR SALE (stock
before
—after
Create roomCreateCreates a predefined room(itthat exists before anyonejoins). Name: A-Z 0-9 _ - (stored uppercase); LOBBY is reserved.joins.`permanentMore:true` keeps it (and its data) when it empties. `ttlInactivity` gives it its own inactivity time (days, 0 = never deleted); without it the app's value for public roomsneblla_get_llmsapplies.tools/create_room
before
—after
CreditsCredits (business template), the administration side: action "balance" { owner, kind? } or "adjust" { owner, kind?, delta }Adjusts(arefunduser'sbycredits;hand,answers agift; never below 0).linkCredittokindsapproveandin theproducts that top them up: neblla_update_app({ business: { credits: { <kind>: { products: {
before
—after
Decide fog caseYour word on a Fog case. "flag_others": the flag moves from the odd one toRecords theFogs ofdeveloper'sthedecisionmajorityon(acoordinated attack — rare).Fog"annul"case:theflaggoes and Neblla reviewsthecase, states included (you saw a bug where the anticheat saw a cheat). Leaving a case alone keeps itothers
before
—after
Delete accountThis is the DEVELOPER own Neblla account, not an end user of their app — to sanction a user use neblla_set_ban_status instead. For safetyGives theMCP never deletes accounts directly. Returnsdeveloper aURL for the userlink tovisit and confirm deletion from the Neblla dashboard. Deleting it deletes every app of theirs as neblla_delete_app does (users, saves, purchases, subscriptions, domains) and their Neblla billing in Stripe;delete their ownStripeNeblla accountand Play listings stay theirs to close. Make sure theyinknowthebefore
before
—after
Delete api secretDeleteDeletes an APIsecret; the app's calls to it answer secret_not_foundsecret.fromMore:thenneblla_get_llmson.tools/delete_api_secret
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨16 unchanged words⟩
before
—after
Delete appPERMANENTLYPermanentlydeletedeletes an appand allwith itsfiles. This is the most destructive call in this server and it cannot be undone: thefiles,the version history, the room data, the community entriesandthe users' saved data all go. Only call it when the developer has asked for that specific app to be deleted, in as many words. Before you do: get their code out (neblla_versions migrate, or neblla_git) and remember paying customers lose their entitlements, so settle any refunds in Stripe first. Neblla itself cancels the app's Pro subscription and its users' subscriptions, removes its custom domain and its history. A Google Play listing is on the developer's own Play account anddata.staysMore
before
—after
Delete automationDeleteDeletes an automation. More: neblla_get_llms tools/delete_automation
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨16 unchanged words⟩
before
—
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
Delete communicationPermanentlyremoveremoves a communication from a user's thread.The app receives `communicationUpdated` with `actionMore:"deleted"` in real time, so its UI can drop theneblla_get_llmsmessage.tools/delete_communication
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨28 unchanged words⟩
before
—after
Delete community entryWithdraw an entry of yourWithdrawsapp'sa community entry for good: the entry, with its payload,itsreviews andfavorites are deleted and its bytes freed (entries hanging from it stay, without their parent). This is not moderation — no reason, no record, nothing counts against the author. To take down a user's entry for breaking the rules, usefavorites.neblla_moderate_community_entryMore:(remove)neblla_get_llmsinstead.tools/delete_community_entry
before
—after
Delete fileDelete a single file fromDeletesanoneapp:file; itgoes withleaves thenext neblla_promote "publish" (in anpublished applinked to its own repo, a commitwithonthepre'snextbranchpublish.that
before
—after
Delete hookDeleteDeletes an incomingwebhook; its URL answers hook_not_foundwebhook.fromMore:thenneblla_get_llmson.tools/delete_hook
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨16 unchanged words⟩
before
—after
Delete productPermanentlydeletedeletes aproduct fromproduct.aMore:Nebllaneblla_get_llmsapp.tools/delete_product
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"productId":{"description":"The ⟨11 unchanged words⟩
before
—after
Delete roomDeleteDeletes a persistent room and its stored data.If people are inside it right now the session continues and the room disappears when it empties.LOBBY cannot be deleted.Irreversible — confirm with theMore:developerneblla_get_llmsfirst.tools/delete_room
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨16 unchanged words⟩
before
—after
Delete room messageDeleteDeletes (soft) one chat messagefromof aroom, as the developer. Usersroom.seeMore:itneblla_get_llmsremoved.tools/delete_room_message
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"
before
—after
Developer emailAddAdds an email to the developer's account(it stays pending,untilresends itsinbox owner clicks theconfirmationlink),orresend that confirmation. Removing emails and changing the primary one are done in the web dashboard. `update_stripe_email` pointssets the Stripecustomer (where the Proreceiptsgo) at the primaryemail.addressMore:when neblla_developer_profile says `stripeEmailDiffers`, ASK the developer first ("Do you want us to update your email in Stripe too?") and call it only on a yes. It never touches their payout account, which is theirs in
before
—after
Developer noticesThe developer's Neblla inbox: notices from Neblla to every developer, such asDismisses achange in the listnotice ofsub-processors announced 30 days ahead (theDPA). action "list" (default) shows the ones not dismissed; "dismiss" with an id hides one. Telldeveloper'stheNeblladeveloperinbox.whatMore:theyneblla_get_llmssay.tools/developer_notices
before
—after
Developer profileReadReturns the developer's profile:(public name— a studio or company name,shown as the author of their apps and in the gallery; the email is never shown —, dashboardlanguage,whether a Claude API key is saved,emailson the account) or change `name` / `lang`. Changing the name also updates the owner name on all their apps. Also their STUDIO (C213): the public page of all their published apps,play.neblla.com/studio/<studioSlug>, each app with its rating, reviews, subscription and Play; it is the business web on their Stripe account, and «by <name>» in the gallery links to it. The studio's name is `name`; `studioSlug` its short name (3-30, lower case letters, digits, hyphens; Neblla proposes one from the name), optional `about` (who they are, since when; up to 1500 characters, in their own words), `web` (their officialsavedsite)
before
—after
DomainsThe developer's own domain, on THEIR Cloudflare (the topic "domain" of neblla_projects cloudflare_setup), and the mail of that domain (topic "mail"). Works with any registrar. Omit projectId for the account's. DNS (free): "add" { domain }: Neblla makes the zone on their Cloudflare, copies every record the public DNS shows (web, MX, TXT, DKIM…; every one «DNS only», as today) and compares MX, TXT, www and the apex with the public DNS. Clean: status "pending" and the two nameServers the developer puts at their registrar (the one manual step); the old web and mail keep working meanwhile. Not clean: status "mismatch" with check.diffs and no nameservers: tell the developer what differs; "copy" { domain } copies again, "copy" { domain, accept: true } goes on anyway only when the developer says so. "check" { domain }: asks Cloudflare whether the nameservers changed (Neblla also watches, and emails the developer when it is active). "list"; "remove" { domain } (the zone stays theirs on Cloudflare). Web: "point_web" { domain, appId, confirm? }: www to the app (its custom domainAdds,certificate by Neblla)points andthe apex redirected to www. Without confirm it answers what it would replace (the records serving the old web): confirm only when the developer agrees. "unpoint_web" { domain } puts those back. Registration (optional; at Cloudflare's cost, inremoves the developer's
before
—after
DrawsFair draws (C311: a pack of cards, a loot box, a daily prize; declared with neblla_update_app({ draws }), opened byReads theuser with neblla.db.draw(name)). Action "info" { draw }: thefairchancedraws ofeach card, the cost and the free openings. Action "opens" { user? (their id in thean app),draw?, limit?(uppacks,toloot500boxes,50 by
before
—after
Edit fileEditEdits a text fileof an appby pieces,without sending the whole file: a list of `edits`, each an `oldText` to find and the `newText` to put in its place(like your own editor). EacholdTextmust appear exactly once in the file (include a few lines around ittomake it uniquenewText),or pass `replaceAll: true` to change every place it appears. The edits apply in order, each on the result of the ones before; if any fails, none is applied and the answer says which one and why. Pass `baseVersion` (the `version` neblla_get_file returned)withoutsosending the
before
—after
Erase userEraseErasesONEone user ofthisthe app—, for a data-deletion (RGPD) request the user made to the developer. Deletes their saved data and messages in the app store and their record in this app; their review and favourite go too. Their Neblla account and their other apps are NOT touched (that is for the user to deleteGDPR),and purchases are kept as payment records. Irreversible: passrequest.confirmMore:true, and only when the developer asked for this user to be
before
—after
Export usersExportExports the users of an appWITH everything the app holds about them: the fullwithrecordall(savedtheir data,public info,pagecommunications,bystats,page;language,for theemail if it was shared) and their purchase balances. For a developer taking theirappdata with them. Paginated
before
—after
Flag userAddAdds a flag to a userof this appby hand, as the anti-cheatdoes when it catches something (users.flags, the count neblla_list_users shows), or take one back with delta -1 (never below 0). A flagdoes.isOnly a markfor the developer to look at:itnever bans,mutesorwarns the user, and the user is not told. An added flag reaches the developer's `flag` webhook withnotifies.byMore:"developer". Returns the user's flags
before
—after
Get activity logTheReturns the app's activity log,of the last 90 days, newest first: who changed what andwhen — the developer from the panel, their team, and you through this MCP (via "mcp", which the panel shows as "from Claude"): settings, publishing, files, the team and who opens pre, products, the app's people, moderation, the back office screens. Lines { id, at, by (an email), who: owner | team | moderator, access? (the paper of a team member: partner | support | viewer | screens | staff), via: panel | mcp | app, area, action ("app.update", "file.upload", "user.ban"…), detail?, values?, target?, count? }: detail names what changed (the fields, the file, the email), never a secret's value; a settings change (app.update) also keeps in `values` ([{ field, value }]) the new value of the safe fields that really changed — numbers, switches and names such as "ai.tokensPerDay" → 20000, never keys, webhooks, Turnstile or prompts; the same change repeated within 10 minutes is one line with `count`. When the app keeps the moderation log (moderationLog), its moderators' acts come mixed in (who "moderator", via "app", action "mod.<kind>", by their user id and byName their name or email); moderation: false leaves them out. A page at a time: pass the answer's `next` as `before`
before
—after
Get appEverythingReturns everything aboutONEone app:its fullconfigand state,(features,and `modules`: the neblla.*modulesit has; rooms, quotaandcredits today, subscription, the config shown is what is being edited: its pre with the unpublished draft over it; `draft` { count, files, settings, savedAt, dropped } is what waits for neblla_promote "publish" (dropped: changes Neblla took out because they no longer fit, tell the developer which), `env.target` where a publish goes ("pro", the default, or "pre"), `env.pro.promotedAt` when pro last changed; gallery, community, ai…), every URL it answers at (by id and slug on each Neblla host and its Cloudflare deploy), the custom domains registered (the allowed origins added with addDomain — Neblla does not serve the app at them, the developer does), whether a TURN relay is available for the P2P "media" option, and `suggestions`: settings off by default that the app's last 7 days (today and the 6 UTC days before) say would help ([{ id, data, change, pending, live }]; apply `change` with neblla_update_app: it waits in the draft and `pending` = already in the draft or in pre, only neblla_promote "publish" is missing; `live: true` (the moderation log) is the app's own setting, it goes to pro at once and is never pending; `suggestionsError` / `suggestionsFailed` say what could not be checked — an empty list then is not "nothing to suggest"), and `savesWaiting`: how many users have saves waiting in their browser ({ quota: refused for want of quota, they land on their next visit with quota; loop: saving in a loop the device refused }), and `alerts`: what needs the developer now, as the panel's Inicio says it ({ counts: { open, urgent, byTopic }, open: [{ id, type, urgent, title, what, users, todo }] }; history and who gets them: neblla_alerts), and `aiUsage`: what the app's AI spends (neblla.ai.prompt through Neblla and the community's AI check; the browser route never passes through Neblla): { today, month (this UTC month), last (the last 30 days), lifetime, ai, creditsPerCall }, each { calls (answered by the provider), tokensIn, tokensOut, refused (before the provider, no cost) and refusedBy { code: n }, failed (the provider answered an error) and failedBy, credits (what it cost the app's quota), dev / user / prescreen: the calls and tokens on the developer's key, on the users' own keys, and of the community check }. `legacy`, only for an app from before the SDK went by modules: { sdk: [{ name, use, files }] — calls of the old SDK in what the users have, which fail now (`use`: the new name; rewrite them, then neblla_promote "publish"),
before
—after
Get app urlReturns thecanonicalpublic URLfor aofNebllaan app in the gallery.ALWAYS call this after neblla_create_app — never construct the URL yourself. `url` is THE link: what the users open, at <slug or id>.neblla.app (or the app's own domain). A new app is published there at once, and each publish of its draft (neblla_promote "publish") reaches it, unless the developer chose pre (`target`: "pro", the default, or "pre"). `promoted` says whether pro has a version; `draft` how many changes wait unpublished. `pre` is the private link of the app's pre, for theAnswersdeveloperplay.neblla.com/<slug>.onlyMore:where publishes go while they test with target "pre". The slug can be renamed with neblla_set_slug. This is the Neblla-hosted address; an app the developer hosts on their own domain keeps its own
before
—after
Get community configTheReturns the app's resolved community configuration:(mode,who publishes,limits, categories,content types,reportthreshold, recidivism, prescreen, downvotes) plus how much of its community storage cap is used, and `platformHold` ({ at, reason }) when Neblla has put the wholethreshold.communityMore:onneblla_get_llmshold.tools/get_community_config
before
—after
Get community entryFetchReturns one community entryWITHwith its payload(the dev-defined JSON),whatever its status, plus its userreports(open count, count by reason,andeach report with an opaque reporter id). Stats carry favorites, rating, reviews and votes (upvotes, downvotes, score = upvotes − downvotes; a banned user's votes do not count). Who votedreviews.isMore:notneblla_get_llmsshown.tools/get_community_entry
before
—after
Get fileGetReturns the content ofa specific file. Text files (HTML, CSS, JS, JSON, SVG, plain text) are ALWAYS returned as UTF-8 strings — never base64. Binary files (images, fonts, audio) are returned as base64. The response includes an `encoding` field ("utf8" or "base64") so you know how to interpret `content`. Always returns the real current source as it is being edited (the app's pre with its unpublished draft over it, so an edit chains on theonebefore), including for apps you created or published in a previous session. Use this to resume editing an existing app across sessions. Large files are returned in CHUNKS — `content` may be only PART of thefile.After each call, check `hasMore`: if true, call this tool again with `offset` set to the returned `nextOffset`, then concatenate the chunks in order. Decode (for binary) or edit only the FULLY reassembled string. `totalLength` is the total length of `content` across all chunks; `length` is this chunk's length. Text files also carry `version` (of the whole file)More:pass it as `baseVersion` toneblla_get_llmsneblla_edit_file.
before
—after
Get llmsFetchReads the Neblla developer guide(llms.txt), splitbytopic so you read only what the app uses. Call this BEFORE designing or writing any code for a Neblla app. Two sections come first: `empezar` — whether Neblla is the right answer at all: the decision rule, why not to roll your own socket server, app shapes that fit, Neblla à la carte, and what it connects to. Then `core` (the default) — the canonical six-file structure, the events-only client/server contract, and the app shapes (rooms start from one example; three room recipes in salas). Then the sections matching the neblla.* modules the app has: salas (rooms, lobby, matchmaking), servidor (the 3 server modes, state.set, anti-cheat), identidad (login/SSO/scopes/save), social (friends + dev↔user communications), comunidad (user-published entries: neblla.community, moderation), pagos (products/Stripe), ia (neblla.ai routes), publicar (gallery + quota table), referencia (the full API/events tables), mcp (this server's tools + creation flow), index (the table of contents), full (everything at once). Skipping core risks producing incorrect Neblla apps; skipping empezar risks reaching for Neblla where a plain index.html was the better answer, or rebuilding by hand what it already gives you. Working on an existing app? Pass its `appId`: the guide comes cut to the app's neblla.* modules (E15) — only the chapters and sections of the modules it has, and `modules` says which. WHO CAN USE WHAT (needs your token, as the app's owner or one of its team):section
before
—after
Get logsTheReturns the app's log while it runs,the last 7 days (30 with Pro; `keepDays` says which), thelatest first, per environment (env "pro", the published app, by default; "pre", the draft):what to read first when something fails. Kinds: "error" — what the app's own code threw and nobody caught: on a page (source "client": client.js or another of its files; an uncaught promise too) or in a server file's handler on the room's server peer (source "server": lobby.js, gathering.js, room.js), with `file`, `line`, `col`, `message`, `name`, `stack` and the `room`; the SDK sends at most 10 a minute from each page and the same error again as a count. "refused" — what Neblla refused the app, `area` "quota" (the app's daily quota, a player's share, the store full; `event` the socket event or "open", a new visit), "data" (the data rules: tables and saves; `detail` the table or field), "ai" (neblla.ai.prompt or the community check refused or failed) or "score" (a score of neblla.scores that did not enter its ranking: `code` the reason, `detail` the board), with its `code`; a refusal of a signed-in user says who (`userId`, and `userName` when Neblla knows it), and the same refusal of two users is two lines. "webhook" — an outgoing webhook that failed (`event`, `secret`, `path`, `status` or `code`). "limit" — more than 60 lines came in one minute: `count` were left out. Lines { id, kind, env, at (first time), lastAt (last time), count (the same thing again within 10 minutes is one line), what (in plain English)
before
—after
Get moderation logReadReads theapp'smoderation log, newest first:whomoderators namedor removed a moderator (of the app or of a room),each ban, every room message, inbox message or community entry someone other than its author took down. Each line: { kind, by ("dev:<email>" or a moderator's per-app user id), target, room?, detail?bans,at }. `on` says whether the appremovedkeepsmessages.theEmptylog;unless
before
—after
Get room historyReadReads a room's chat history,(newest first,paginated)for moderation.Returns the users' messages in clearMore:only use it when the developer asks to review aneblla_get_llmsroom.tools/get_room_history
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"
before
—after
Get statsTheReturns the app's statistics,(as the panel's Statistics tab),kept only while `playerStats` is on (off by default; turn itshowsonthem.withEmptyneblla_update_appunless playerStats:true, in the act — confirm with the developer, it costs quota). The last `days` UTC days (30 by default), today included, of signed-in players: `cards` { activeToday, activeYesterday, activeThisWeek, activeLastWeek, weekTrend (% vs the week before), dailyActiveAvg7, dailyActiveTrend, newPlayers, newTrend (% vs the period before), visits, avgMinutesPerDay (per active player and day, with the app open and in sight), avgMinutesPerVisit, returns { d1, d7, d30: { cohort, returned, rate } }, money { currency: minor units } }, `series` (one point a day: active, new, visits, minutes, avgMinutes, money), `weeks` (the last six: active, new, money; the last one partial), `sources` and `campaigns` (where new players' first visit came from: utm_source, else the referrer's host, else "direct"; "neblla" = the gallery), `products` (sales and money per product), `bookings` (with booking sets: occupancy per set and per class and time, weekday 0 = Monday, in `timeZone`) and `cost`: what keeping them costs from the visits of now { visitsPerDay, basis, creditsPerDay, dailyQuota, percentOfDailyQuota, eurosPerDay, eurosPerMonth } — ask for it before turning them on. `on` false: nothing
before
—after
Get userFetchReturns thefullrecord ofa singleone end user— their: saved data,public info,purchases,and communications thread.Use the `_id` from neblla_list_users / neblla_search_users_by_emailMore:asneblla_get_llms`userId`.tools/get_user
before
—after
GitKeepLinks an appfiles in sync with a GitHub repo. Each environment is a branch: `branch` is pre's, the source of truth for what is being worked on; `proBranch` (default "neblla-pro") is pro's, and only neblla_promote writes it, by merging the commit pre was last synced from (made on the first promote; a pushtoit changes nothing for the users). One nuance worth getting right: while linked, every file tool (neblla_upload_file, neblla_edit_file, neblla_delete_file, neblla_move_file) still works, and each call isacommit pushed to pre's branch of theirGitHub repowith their token, then synced back, so an agent editing files lands commits on their branch, which is worth agreeing with them first. neblla_versions readsandrestores that repo's pre and pro branches. A sync also DELETES files that disappeared from the repo, so old hashed build chunks do not pile up. Neblla runs no build step: the build output must be committed, because a gitignored dist/syncsnothing. Mind the per-app file cap too (30 on free,150 on Pro). Actions: "repos" — the developer GitHub repos (needs "Sign in with GitHub"); "branches" (url) — branches of a repo; "status"; "link" (url, optional branch, proBranch, subdir, syncMode auto
before
—after
GroupGroups of users with a role each (a team, a client), for rows that belong toCreatesaorgroup:deletesneblla.db.save(data,groups{oftableusers,group }). Rolesandtheir rights per table are declared with neblla_update_app({ roles }). Actions: "create" { name }, "delete" { name } (with its members; its rows stay), "add" { group, user, role } (user: the user id in the app, as neblla_list_users gives it; adding again changes the role), "remove" { group, user }, "list" (the groups,adds or{ group } its members), "of" { user }
before
—after
List api secretsTheLists the app's API secrets:name, baseUrl,withheadersname,query,baselimitsURL andfromClient.limits,Nevernever the values. More: neblla_get_llms tools/list_api_secrets
before
—after
List appsList the apps owned byLists theauthenticateddeveloper'sdeveloperapps, one short line each: id, name, slug, plan(its project's Pro, or free), projectId (the project it is in, neblla_projects), archived, files (how many the developer edits), updatedAtandpageUrl. For an app's configuration and state, call neblla_get_app with its id. `shared`: the apps of other developers whose team they are on (neblla_team), with their groups (`groups`; `access` is the first ready-made one: partner, support, viewer, screens or staff), whether in Business they see only their own (`own`) and what their groups, added up, let them do on each areaproject.(`can`More:read | write | null), and `invitations` waiting for them to accept (in the panel, «Compartido contigo»). On a shared app, use the tools its groups allow; the rest answer
before
—after
List automationsTheListsapp’sthe app's automations withtheirschedule, action, next run andthe resultlastofresult.theMore:lastneblla_get_llmsone.tools/list_automations
before
—after
List communicationsListListscommunicationsmessages acrossALLall users of an app, filteredand sorted. The most useful tool for triaging an inbox: showbyonlyreadunreadstate,only messages from users (not the dev)sender,only flagged-important,importance oronly those mentioning a givenemail.Returns flat items with userId so you can follow up via neblla_send_communication on each thread. An item with `contact: true` is someone without an account who wrote through a contact form (Neblla's form on the developer's web)
before
—after
List community entriesListLists the community entries (user-publishedcontent,neblla.communitypublished by users) of an appfrom the backoffice, with any status.Use it to triage the moderation queue: status "pending" is what awaits approval in review mode. Returns indexed metadata only (title, summary, category, tags, contentType, owner, status, moderation record, and statsMore:favorites, rating, reviews, uses, upvotes, downvotes, score); fetch a payload with neblla_get_community_entry. Paginate with `cursor` = the previousneblla_get_llms`next`.tools/list_community_entries
before
—after
List erasure requestsTheLists the users who asked, from insideyourthe app(neblla.auth.requestErasure),that you eraseforthetheir datathey gave it. Erase one with neblla_erase_user (its userObjectId); that closes thetorequest.bePendingerased.onesMore:
before
—after
List filesListLists the files of an app as it is being edited:itspre with the unpublished draft overit (a file only in the draft saysit.draftMore:true; neblla_promote "diff" says what a publish would change). Returns path, mimeType, size, and timestamps for eachneblla_get_llmsfile.tools/list_files
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"
before
—after
List fog casesTheListsapp’sthe app's Fog cases, newest first:when the Fogs running a room’sroomsserverwhosefileservers computed differentstates, Neblla compared them, the majority won, and the odd one’s user was flagged. Each case: room, seq, status (flagged | undecided | reassigned | annulled), fogs (userId, appUserId, leader, verdict: majority | odd | missing | undecided), diff (only the paths that differ), flagged.states.UndecidedMore:no majority (two Fogs, or three different states) — nobody was
before
—after
List hooksTheListsapp’sthe app's incoming webhooks: name,to, room. Nevernever theirtokens (make one again fortokens.aMore:newneblla_get_llmsURL).tools/list_hooks
before
—after
List moderatorsListLists the app-levelmoderators (per-app user ids). App moderators manage rooms of any type and moderate the community. They are the members of the app's reserved group `moderators`, which lives in itsmoderators.dataMore:a pre on pro's data has pro's, a pre with its own data its own (empty at first; env "pre" to seeneblla_get_llmsit).tools/list_moderators
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"
before
—after
List productsListListsallthe productsfor aofNebllaan app. More: neblla_get_llms tools/list_products
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
List reviewsListLists the gallery reviews of anapp (a review id — pass it as reviewId to neblla_reply_review, rating 1–5, text, language, author display name, the developer's reply) plus the average, count and 1–5app.distribution.More:Newestneblla_get_llmsfirst.tools/list_reviews
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨10 unchanged words⟩
before
—after
List roomsListLists the persistent/ predefinedrooms of an appwith their: type, owners,whitelist,moderators, data,message count, last activity, their own inactivity days (`ttlInactivity`, null = the app's per-type value), how many players follow each one (`followers`: they get a notice when someone writes while they are away)andhow many users are in eachactivity.oneMore:rightneblla_get_llmsnow.tools/list_rooms
before
—after
List usersListLists the end users of an app— the people who connected through the SDK, NOT(not the developer's ownaccount. Use this as the entry point for the backoffice flow: it returns a paginated list with each user's mongo `_id` (needed for every other backoffice toolaccount),email, name,withlanguage,email andstats. Passname.`withCommunicationsOnlyMore:true` to get only users who have communication threads — useful when triagingneblla_get_llms
before
—after
List webhook deliveriesTheListsapp’sthe app's last outgoing webhook deliveries(neblla_update_app({ webhooks })), newestfirst: event, secret, path, at, ok, and the endpoint’s statusfirst.orMore:theneblla_get_llmserror.tools/list_webhook_deliveries
before
—after
MatchesResults of matches (C352): a service with `results` ({ teams: 2, reportHours: 48, board?, levelRange?, cheaterHint?, cheater? } in neblla_update_app({ business: { booking: { <service>: … } } })) makes each of its sessions,Settlesonceorplayed,annuls a matchbetween side "A" and "B"; and with neblla_update_app({ matches }) (C372) each game a room starts is one too (its id room_…,its title the room's name), reported with neblla.room.matches.report; its players say who won with neblla.business.matches.report. When they disagree it is disputedandcounts for nothing until the developer settles it. Action "disputes" → the queue, oldest first: { match, set, title, start, deadline, players: [{ user, name, team, said: { winner, score? }, tally: { reported, disputed, lost, ratio, hint, cheater? } }], chat? }; "resolve" { match, winner: "A" | "B" | "draw", note? } (also a match nobody agreed on yet; it moves the ELO board of the service now); "annul" { match, note? } (it no longer counts; the board is counted again without it); "player" { user } → their tally (hint: true when they pass the
before
—after
MediaThe MEDIA of an app: photos, textures, sounds, videos and 3D models (glb), many, apart from the app's files (neblla_upload_file is for code, 300 files for a free account's apps together, each counted once per app whether in pre, pro or both, with versions and neblla_promote). Media have no versions and no deploy: a change is live at once, in pre and pro, and the app reads them with neblla.media (it sees the change on its next read, without touching its code). They are in COLLECTIONS (made when first named), in an order, each item with a card: title, text, alt and your own fields. An image gets WebP variants (320, 640, 1280, 2048 wide, never wider than the original) and keeps its original as it came. The space is a bag of the ACCOUNT: 100 MB shared by the apps of its free projects, originals and variants (a Pro project has its own 2 GB for its apps). One file up to 25 MB. Actions: "collections" → { collections: [{ collection, key, count, bytes }], usage: { scope, used, limit, free } }; "list" { collection, offset?, limit? } → the items in order, each { id, position, title, text, alt, fields, width, height, url, srcset, variants, original, bytes }; "get" { id }; "upload" { collection, items: [{ contentUrl | content, title?, text?, alt?, fields? }] (up to 50), position?: "end" | "start", resize? } → { uploaded, skipped?, failed?, oversized?, usage }: each item on its own (a failed one says why, the same bytes already in the app are skipped with the id that has them); TOO BIG FOR A SCREEN: the original is kept as it came. An image over 2560 px on its long side carries "oversized" (and the upload a summary "oversized"): what it takes, what it would take reduced, what that saves and how many more would fit in the bag, with a message. Tell the developer those numbers and ASK whether to reduce: never decide it yourself, a texture, a map or an original to print may need every pixel. "resize": true (to the screen size) or a long side in px reduces at upload, when the developer already said so. "shrink" { ids | collection, maxSide?, dryRun? } reduces later the images over maxSide (default 2560), up to 50 per call ("remaining" says how many are left): the item keeps its id, card and place, its original is replaced and its variants made again, so THE BIG ORIGINAL IS LOST (say so, and confirm first). dryRun: true says what it would save without changing anything. "update" { id, title?, text?, alt?, fields? (merged; null removes a field), replaceFields? }; "move" { ids
before
—after
Moderate community entryModerate a community entry as the app owner: approve or reject a pending entry, remove a published oneApproves,suspend a published one while a case is openrejects,or restore a rejected/suspended/removed one. `suspend` hides the entry but is reversible and keeps everything; `remove` is the final one. `reject`removes,`remove` andrestores`suspend`orrequiresuspends a`reason` from the fixed taxonomy; the author receives the reason and your `message` live (communityUpdated) and in their own list. `reason` is one of: copyright, inappropriate, hate, harassment, spam, illegal, privacy, other. Note which queue youcommunity
before
—after
Move fileRenameRenames ormovemoves a file within an app.oldPath is the current path, newPath is the destination. It waits in the draft until neblla_promote "publish". In an app linked to its own repo, one commit onMore:pre'sneblla_get_llmsbranch.tools/move_file
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨34 unchanged words⟩
before
—after
OrdersThe orders of the app's physical products (a product with `shipping`, neblla_create_product; Dinero › Ventas in the dashboard). Each paid physical product isMoves an orderwith the buyer's addresson (name, phone, address), the zone and postage paid, and a status: paid →shipped(with carrier and tracking number,and its link) →delivered,orreturned(from any of them).action "list" { status?, skip?, limit? } — newest first, each with its withdrawal ({ until, open, daysLeft }: the EU's 14 days from the delivery, during which the buyer may withdraw), refundedorwhencorrects
before
—after
Payments accountThe developer's payout account (Stripe Connect), needed to sell products inside their apps. One account per developer, for all their apps, unless a project has its own (neblla_projects "use_own_stripe"): pass projectId to act on that project's account instead; every action below works the same on it. "status": account none | pending | active | restricted, with chargesEnabled / payoutsEnabled, and what is MISSING in words, one line per thing (missing: [{ key, words, overdue }], errors Stripe found, verifying, filledByNeblla: what Neblla already put there — email, business name, web, the statement prefix), and `message`, a sentence to tell the developer in their language. It asks Stripe every time: call it again when the developer says they finished the form. With no account it says why (lostAccount: the stored one is not on Neblla's Stripe, e.g. one opened in test mode) and onboarding_link opens a new one. stripeReachable: false means Stripe did not answer (error.kind "down": try again) or refused (error.message says why); it is never a silent pending. "onboarding_link":Opens theneblla.com page where Stripe'spayoutownaccount's onboardingopens inside the developer's Neblla session (no app: the account serves all their apps), where they verify identity and add a bank account (a human must do it; never ask for bank details
before
—after
Pre accessWho else may open the app's PRE (its private test version at <slug or id>.neblla.app/pre/). The developer always can. Two ways in: Neblla accounts by email (they sign in on the pre page), and links with a password for people without an account. Either can have an expiry. Once in, a person keeps a pass for 4 hours even if they sign out, so they can play as a guest; removing someone stops new passes, not one already given. Actions: "list"; "add" (email, expiresAt?) — let an account in,Gives orchange its expiry; "remove" (email); "link" (password?, expiresAt?, label?) — a new link; without a password one is generated. The password is returned ONCE: giveremovesitaccess to thedeveloper and never store it; "revoke" (linkId); "renewLocalKey" — a new local key, the old one stops working. Each action returns the current list. The list also has `local`: { key, tag }, theapp'slocal key. A page on localhost (the developer's machine) opens only the app's PRE, and only with the tagprivatethat
before
—after
Pre dataWhichSets which data the app'sPRE (its private test version)pre plays with."own" (the default): its own users, saves, tables, inbox and community, empty at first and apart from the users'. "pro": the users' real data, read and written for real. Rooms are always pre's own, so testers never share a live room with users. Changing it signs out whoever is signed in to pre. Without "use", returns the current one. Switching to "pro" lets pre's unreleased code write the users' dataMore:only when the developer asked forneblla_get_llmsit.tools/pre_data
before
—after
Pre stripe testWhether the app's PRE sells through Stripe's TEST mode: on by default, so testers buy withTurns Stripe's testcards and nothing is charged. Pre always sells the app's own products (one catalogue); in testmodeit also lists the ones not published yet. Test purchases are marked as such: pro never counts them, not even when pre plays with pro's data (neblla_pre_data), and there they give no credits. Without "on",returns the current value. Also says "testMode" ("separate": the developer needs a test account of their own, opened at onboardingUrl with Stripe's test data; "same": this server is in test mode and the payout account serves;or"off":no test mode here, so pre test purchases fail) and "account"for(the
before
—after
ProjectsThe developer's projects: account → projects → apps. Every account has the project «Explorer» (it can be renamed, not deleted) and new apps land there; a developer who never creates another sees nothing new. The FREE quota is the account's: one pool of credits a day, store and files shared by every app of its free projects (making empty apps does not multiply it), and each app may carry a cap of its own inside the pool (set_app_caps). Pro and the quota packs are the PROJECT's (neblla_billing with projectId): a Pro project has its own pool and its apps stop drawing from the account's. Example: the chess spends 1500 credits a day of «Ajedrez»'s Pro and the free pool stays whole for the yoga and the tests. Connections: Stripe (who the apps sell as), Cloudflare (one token for the developer's server boxes, neblla_byos) and the database (where the apps keep their users' data, as neblla_byodb) are set once on the ACCOUNT, which Explorer uses, and a project may put its own and inherit the rest (example: «Ajedrez» sells with another Stripe and keeps the account's Cloudflare). Omit projectId to act on the account's. Actions: "list" (projects, their apps, plan, pool and connectionsCreates,with where each comes from: project or account); "create" { name }; "rename" { projectIdrenames,name }; "delete" { projectId } (emptydeletes andnot Pro); "move" { appId, projectId, confirm? }: a real move — the quota counts in the new project from now, the data
before
—after
PromotePublishing (C272, C273). Every change you make toMoves an app(uploads, edit_file replaces, update_app settings, retheme…) and every change from the dashboard waits in the app's DRAFT: nobody sees it, not the users and not pre's testers. Nothing is ever published on itsbetweenownenvironments:CALL "publish"WHEN A CHANGE IS COMPLETE (all its files together) AND ALWAYS BEFORE YOU FINISH YOUR TURN. The app's own settings (name, slug, publishing, bans, team, domains) are not inthe draft: they change at once. Each app has a chosen environment,`target`: "pro" (the default: what the users have) or "pre" (a private copy the developer tests calmly, at the pre link, with its own data and Stripe's test mode). Actions: "status" — { target, draft: { count, files: [{ path, op: put|delete }], settings: [field], savedAt, dropped }, pre: what pre has that pro does not (only with target
before
—after
Publish appPublish a Neblla app to the public gallery. BEFORE calling this tool you MUST follow these steps with the user: 1) Ask forLists the apptitle and a short description (1-3 sentences)in theuser's language. 2) Ask for the gallery section: "multiplayer" (games/apps where users interact in real time), "singleplayer" (games/tools for one person at a time), or "app" (everything else — utilities, tools, sites). 3) Ask for tags that describe the app (e.g. "puzzle", "multiplayer", "drawing"). 4) Ask what devices the app is best suited for: "mobile", "desktop", or omit for both. 5) Ask if the user wants to upload a cover image — pass an http(s) URL,publicagallery.dataMore:URI, or raw base64 JPEG/PNG. It will be auto-scaled to 400x400. 6) Provide the title and description in at least the user's language. The app is listed in the gallery ONLY for visitors whose language is one of those given (en, es, fr, de, it, pt, ja, ko, zh, ar — the languages of the Neblla website and gallery; they limit only the listing, never the app, which can use any language), and nothing is translated for you. Add other languages only if the developer wants visitors (and reviews) in them, and can read what those visitors write. Set defaultLang to the language the user originally provided the content in. titles and descriptions MUST each contain at least one entry in the same
before
—after
Publish community entryPublishPublishes a community entryAS YOURSELF — ownedasbytheyourdeveloper's own user in theapp (the app user whose email is your developer email; it is created on first use if you never opened the app). Use it to seed a community with starter content. It skips the review queue (you are a moderator of your app) and obeys the same rules asapp.neblla.community.publishMore:community must be enabled, contentType/category must be allowed, payload ≤ maxPayloadBytes, no "$"/"."neblla_get_llmskeys.
before
—after
Publish storeThe developer’s app as its OWN app in the stores, with THEIR developer account (Android now; iOS after). store "google": action "set" { packageName (e.g. com.studio.game; fixed once published), serviceAccountJson (a Play service account with release rights, kept sealed), track?Sets(internal|alpha|beta|production)up,certs? (the app signing SHA-256 from Play Console → App integrity: the app then opens full screen) }; "build"buildsthe Android bundleorfromremoves theapp’s page (<slug or id>.neblla.appapporas its owndomain) and uploads it to that track — it runsapp
before
—after
Reply community reviewAnswerAnswers a review ofan entry in your app'sa community,entry as theapp:app; the answer shows under thereview for everyone. One answer per review; sending another replaces it, and text null takes it back. reviewerId is the review's author.id (neblla_get_community_entry lists the latest reviews). The entry's author can answer from thereview.SDKMore:tooneblla_get_llms(community.reply).tools/reply_community_review
before
—after
Reply reviewPublishPublishes,(replaces orreplace)deletes the developer's public reply to areview; pass `text: ""` to delete the reply. The reply is shown in thegallery,translated into the reviewer's language when `developerLang` differs. Publicreview.textMore:show the developer the wording beforeneblla_get_llmssending.tools/reply_review
before
—after
Retheme appChangeChanges the paletteand/orand typography of an appcreated with the wizard or neblla_create_app, AFTER creation. Re-bakes only the theme tokens (--nb-bg, --nb-primary, …, --nb-font-title, --nb-font-body) in main.css and the Google Fonts link in index.html; every other line stays as the developer left it. Tokens the developer deletedafterareitnotwasputcreated.backMore:they come back in `missing`. Stores the theme on the app. The files and the theme wait in the draft until neblla_promote "publish". (neblla_update_app with `theme` only stores it, without touching the
before
—after
Run automationRunRuns an automation now, to tryitit;(its schedule does notchange).change. More: neblla_get_llms tools/run_automation
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨16 unchanged words⟩
before
—after
Run panelSeeShows a panel as the back officeshows it: { appIddoes,name,withinputs?its}rows;→it{runstitle,nocolumns,panelfilters,action.
before
—after
SalesThe app's sales (Dinero › Ventas in the dashboard): action "list" { status?, productId?, from?, to?, seller?, skip?, limit? } — newest first, each with product, units, paid (minor units) and currency when known, status (completed | refunded | charged_back | ended), refund and dispute, the buyer's userId, email and buyerName, canRefund, and the seller who was paid (seller, sellerName: one of the app's sellers, neblla_business "sellers"; none: the app). "documents" { saleId } — the links Stripe keeps for the buyer: a payment's receipt (receiptUrl) or a subscription's invoices (url, pdf); nothing isAsksstored.to"refund"{ saleId } — gives the WHOLE payment back in Stripe (Neblla's 1 % with it;aseller's sale is given back by the seller, with the developer's commission when it can be recovered) and takes the purchase back at oncesale;(theplayerdeveloper
before
—after
Sample usersSample users of an app: invented clientsCreates,playerschanges ormembers so its back officeremoveslooksinventedfullusers andreal without showing anyone real (thestudents of a yoga studio, the clients of a dog groomer, the players of a ranking): an example app's screenshots, or a studio showing its client how the app will look with people in itbookings,best in its prepurchases (env "pre"). Remove them all with removeAll before the app opens to the public. They have no account: nobody signs in as them and no mail is sent to them; their address ends in @ejemplo.neblla.app. They count in no quota
before
—after
ScoresThe rankings of a game for one (neblla.scores; boards declared with neblla_update_app({ scores })). action "top": the best of a board, { board, period: "all" (default) | "day" (today, UTC), limit (≤ 100) } → [{ place, player, value, at, info }]; action "player": one user's standing { board, player } → { best, bestAt, dayBest, runs, place } or null; action "remove": takeRemoves auser'sscoreoff a board { board, playeror}sets(acheat that got in). Refused scores never enter: they are in the app log (neblla_get_logs, area score). An ELO board ({ type: "elo", start: 1200, k: 32 }, C353) moves with the results of matches (neblla_matches): "player" gives { rating, played, won, drawn, lost, place, history }; action "setRating" { board, player,player's rating} sets whereon aplayer starts (their level known elsewhere), and the matches after
before
—after
Search users by emailFindFinds users of an app byapartpartialofmatchemail (min 23 characters).Returns the matching user docs with email, communications, data,orstatsmore,and language. Use neblla_list_users for full pagination instead
before
—after
Send communicationSendSends a message from the developer toa specificone enduser. The user receives it live via the SDK's `communicationUpdated` event if connected, and on next reconnect otherwise. Optionally attach a `data` JSON payload (rendered by the app), or a calendar/appointment proposal via dateFrom/dateTo. Set `thread` to the id of an earlier communication to group replies. Touser'ssomeoneinbox.withoutEmailanonlyaccountfor(`contactcontact
before
—after
Server keyThe app's server key, for running room.js on the developer's own server (serverMode "own", see neblla_get_llms({ section: "servidor" }) "Server"). Their server runs `npx @neblla/server --app <appId> --key $NEBLLA_SERVER_KEY ./server`: it calls NebllaCreates,no open portsrotates orpublic URL. action "create" makes the key (refused if one exists), "rotate" replaces itrevokes(theold one stops at once), "status" says whether one exists, "revoke" removes it. The key is returned ONLY byapp'screateserverandkey.rotateMore:
before
—after
Set api secretCreateCreates orchangechanges an API secret: an external API the app reaches withneblla.api.call(name, path, body) without a server of yours. Neblla fills the secret in where {secret} appears in `headers` or `query`, and calls only under `baseUrl` (https, a public host). The value is written and never read back (neblla_list_api_secrets shows everything else); on a change, leave `value` out to keep it. From the server file (room.js…) it always works; from a user's client only with fromClient: true.neblla.api.call.LimitsMore:loggedInOnly, calls per user per minute / hour / sixHours / day / week / month, and a whole-app cap per day / month (0 or absent = none). The bytes both ways count asneblla_get_llmsquota.tools/set_api_secret
before
—after
Set automationCreateCreates orreplacereplaces an automation: a scheduled action on theapp (open a room, hand a room data, call one of your API secrets). For flows across the tools the developer already uses, n8n, Make or Zapier connect through the admin API, incoming webhooks and outgoing webhooks. when: { at: ISO date } once; { every: minutes } (5 – 10080); or { daily: "HH:MM", days?: [0–6, 0 = Sunday], tz?: "Europe/Madrid" (UTC by default) }. action: { kind: "openRoom", room, config?, openForMinutes? (60) } books the room with its config, so whoever enters first finds it in neblla.room.config; { kind: "message", room, data } hands data to that live room’s server file as neblla.room.on("hook", ({ name, data }) => …) (name = this automation); { kind: "callApi", secret, path, method?, body? } calls one of your API secrets (neblla_set_api_secret). Up to 50 per app; each run is recorded in lastResult. Or, instead of action, a ready-made template: template "booking.reminder" { set, secret, path?, hoursAhead? (24) } — one call per booking starting soon, each once ({ event, player: { id, name, email if granted }, slot, booking }); "community.digest" { days? (7), limit? (10), secret and/or room } — the week top entries; "game.dailyTournament" { board, day? ("today" | "yesterday", UTC), top? (10), secret and/or room } — the day's top of a protected ranking (neblla.scores, declared with neblla_update_app scores), kept and announced as { event, board, day,
before
—after
Set ban statusSetSets a user's ban state inthisthe app:"none" (lift),"banned"(the app shows the ban message and blocks play; Neblla refuses everything but their messages to you), "terminated" (blocks them for good: their device is sealed so the app no longer opens there, and they are disconnected), "community_muted" (can play, and can still REPORT entries, but cannot publish, rateorreview). Takes effect live. WARNING:terminated. "terminated" seals theuser's device, and lifting it with "none" does not unseal a device that is already sealed (the user has to clear the site's data) — never pass it unless the developer asked for that specific value. The other two are fully reversible withdevice;
before
—after
Set communication importantToggleSets or clears the"important"flagonof a communication.This is dev-only metadata and is NOT broadcast to the end user — use it to bubble messages to the top of your ownMore:triageneblla_get_llmsview.tools/set_communication_important
before
—after
Set moderatorMakeMakes a user an app moderator(`moderator: true`) or remove them (`false`): it adds them to, ortakes them out of, the app's reserved group `moderators`. `userId` is the per-app user id (from neblla_list_users with the same env), not an email. The group lives in theremovesapp'sthem.dataMore:with env "pre" and a pre with its own data, it names a moderator of pre (the developer adds themselves to moderate there); a pre on pro's data sharesneblla_get_llmspro's.tools/set_moderator
before
—after
Set panelCreateCreates,replacereplaces or(panel: null) removeremoves a back office panel: a screen for the developer and their team.panel: { title, source: { table } | { booking: set } | { credits: kind }, columns: ["field" | { field, label }] (player.name, start, title… too), where: { field: value | { gte: "$today", lt: "$tomorrow" } } (dates: $now, $today, $tomorrow, $weekStart, $monthStart, $nextMonthStart), filters: [{ field, op?, label?, type? }] the person fills in, orderBy, limit (≤ 500), actions: [{ label, call: "booking.cancel" | "booking.move" | "credits.adjust" | "table.remove" | "table.save", args: { … "$row.field" | "$input.name" }, confirm? }] — on each row; or call "booking.createSlot", a button over the panel with a form (title, start, minutes, capacity, and how it repeats as in a calendar: freq, interval, byDay, byMonthDay, until or count), args filling it in beforehand }. e.g. "Reservas de hoy": { source: { booking: "clases" }, where: { start: { gte: "$today", lt: "$tomorrow" } }, columns: ["start", "title", "player.name"], actions: [{ label: "Cancelar", call: "booking.cancel", args: { refund: true }, confirm: "¿Cancelar?" }] }. A { booking } panel lists every slot of its range, free or not: a row per booking (with capacity and left, the places left) and one row with status "free" and no id for a slot nobody booked; row actions only on booking rows. Panels see every table and user
before
—after
Set slugCheck, setSets orclearclears the app's public slug (the readable name in <slug or id>.neblla.app). `checkOnly: true` only tells whether it is valid and free; when another app of YOURS holds it (its slug or an old one) it says so (`reason: "yours"`, `heldBy`play.neblla.com/<slug>).`slug: null` removes it. AnThe old slug keepsleading to the app (it stays the app's; nobody else can take it), and the users' sessions follow. `move: true` takes a slug from another app of yours (e.g. moving a gameworking;toappliesaatnewonce.
before
—after
Table queryReadReads an app table across users: filterson declared fields, order, page,orjusta count.e.g. { table: "reservas", where: { clase: "yoga-martes", estado: "confirmada" }, count: true }, or { table: "puntos", orderBy: "-score", limitMore:10 }. Rows come with id, owner (the user id in the app), createdAt,neblla_get_llmsupdatedAt.tools/table_query
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"
before
—after
Table rowWriteWrites orremoveremoves one row of an app table byhand (support, ahand.fix)More:action "save" with { owner, data } for a new row of that user, or { id, data } to replace an existing row; action "remove" with { id }. Users never see other users’ rows; this is the developerneblla_get_llmsside.tools/table_row
{"$schema":
before
—after
TaxTaxes onChanges thedeveloper’stax settings for sales to users, with Stripe Tax on their own payout account (they are the seller; the taxes are theirs, Neblla only sets it up for them). "status":settings status (active/pending, and what is missing),head office, defaultsand active registrations. "set_head_office": the business address ({ country,postal_code, city, line1, state? }). "set_defaults": the default product tax code (txcd_…, e.g. txcd_10000000 for general digital goods — check with the developer)registrations andtax behaviour. "add_registration" / "expire_registration": the jurisdictions where the developer is registered to collect tax ({ country, type, state? } — type as Stripe names it, e.g. "standard", "oss_union", "state_sales_tax"); with no registration in the buyer’s location no tax is collected. "report": itemized tax transactions — call with { from, to } to start it, then with { runId } until it returns a download link. "app": switch automatic tax on or off for one app’s checkouts ({ appId, automatic, inclusive? }); it only applies while the settings are active. By default the price INCLUDES the tax (what a European consumer expects); {
before
—after
TeamThe developer’s team for an app (Negocio › Equipo in the panel): people who sign in to developers.neblla.com with their own email (any Neblla account, not only a developer's)Adds,and use this MCP with their own account. Each member is in oneremoves ormore GROUPS of the team, and their rights ADD UP (a partner who also teaches is in «partner» and in a group of teachers); the server applies them on every route ofregroups thepanel and every tool here, and a call no group of theirs allows answers not_allowed. A group says, per areamembers of thepanel (home, stats, people, business, moderation, team, testers, panels, features, money, build, publish, settings), "read" or "write" (nothing when absent); team and money can only be read: the owner alone changes thedeveloper's teamand the money, and archives or deletes the app. `own: true` narrows Business to what is the member's own (the sessions they give and who is in them, their hours, their line of the accounts; they are the professional of neblla_business kind "staff" with the same email); `panels` gives custom screens by name. Ready-made groups (templates, and their ids work as groups as they are): "partner" (Socio: everything but money, the plan and deleting the app), "support" (Comunidad y soporte: the people of the app, the Business tab, and the sales, read only), "viewer" (Solo mirar: everything, read only), "screens" (Solo una pantalla: only the custom screens in `panels`) and "staff" (Profesional: Business, only what is theirs). action "add" { email | friend, groups?: [ids], access? (one ready-made group, as before groups), panels?: [names] | "*", role?, expiresInDays? } invites them (someone new without groups is in "screens"): an email goes out and they accept it signed in with that address, within expiresInDays (1-30, 7 by default);
before
—after
TradesTrades between the app's users (C170: neblla.db.offer / acceptOffer; what is tradable is neblla_update_app({ trade })). Action "list": the log, newest first, with { user? (their id in the app), status? (pending | done | declined | cancelled | expired | failed | all, the default), limit?, offset? } → { trades, total };Cancelseacha tradehas from, to, give, get (items { table, id } or { product, amount }), status, reason (why it failed or was cancelled), createdAt, expiresAt, closedAt.betweenActionusers."cancel"{ id }: takeonly a pendingoffer back (support, abuse); both users hear it. A trade that was done is notoffer.
before
—after
Unpublish appRemoveRemovesanthe app from the publicgallery. The app keepsgallery; its files and URL— only its public listing is hidden. Usestay.neblla_publish_appMore:toneblla_get_llmsre-publish.tools/unpublish_app
before
—after
Update appUpdateChanges an app'sconfiguration. Pass only the fields you want to change. Use addDomain/removeDomain to mutate the allowed-origins list atomically without overwriting other entries. `compiled: true` lets a compiled app (Capacitor, Ionic, Tauri, Electron) use the app: it takes their origins (capacitor://localhost, https://localhost, null…) without a domain, and its new guests are limited per address. A page on localhost opens only the app's PRE, with its local key (neblla_pre_access). `turnstile`: Cloudflare Turnstile before a new guest is made (stops scripts creating guests in bulk; returning users are not asked): true turns it on: with the developer's own widget if one is saved, else Neblla's, which works on <app>.neblla.app and play.neblla.com but not on a domain of the developer's (turnstile_own_widget_required; turnstile_unavailable if this server has none); { siteKey, secretKey } uses the developer's own widget, for a domain of theirs (secretKey is never read back; leave it out to keep it); false turns it off and keeps their keys; null turns it off and forgets them. `ai` is the AI proxy config for the SDK's neblla.ai.prompt() — set { model, maxOutputTokens, userTokenMode, presets, limitsEnabled, tokensPerDay } or null to disable. userTokenMode is onegeneralofsettings:'off' (server uses dev key only), 'browser' (user key, browser-direct), 'server' (user key, server-side with private preset prefixes). presets is a {
before
—after
Update community entryEditEditsanyan entry ofyourthe app's community,—itsyoursown or auser's — as theuser's.developerMore:title, summary, category, tags, lang, i18n (replaces the whole map; null clears it) and/or payload (replaced whole). Only the fields you pass change. Editing a user's entry records that you did and tells them. Use it to keep a community fed from elsewhere inneblla_get_llms
before
—after
Update productUpdateChanges an existingproduct. Passproduct; only thefieldsgiventofields change.Use this to adjust price, rename, or republish a product without losing its productId — changing the id would break checkout flows already wired intoMore:theneblla_get_llmsapp.tools/update_product
before
—after
Update roomChangeChanges a room:replaceits shared`data` (users in the room receive it live)data, its`moderators` listmoderatorsand/oror itsowninactivitytime (`ttlInactivity` in days, 0 = never deleted, null = back to the app's per-type value). Replacing data overwrites the live game state of that room — confirm withtime.theMore:developerneblla_get_llmsfirst.
before
—after
Update user dataREPLACESReplaces (does not merge) the`data` objectsavedfor`data`aofspecificone enduser (the same object the SDK's neblla.db.save() writes and neblla.db.data shows). There is no merge option here, unlike the SDK which offers shallow/deep/crdt: whatever you pass becomes the whole document, so read it first with neblla_get_user, modify, and write it back — and be aware the user may be saving at the same moment. Useuser.sparinglyMore:this overwrites the user's saved state and the change is broadcast live. Useful for support (unstuck a user), moderation (clear a corrupted save), or admin tooling. If the app has a "bring your own database" target configured, this writes there. Mongo-reserved keys (starting with $ or containing ".") are rejected; total JSON size is capped at ~100KB and 10 levels
before
—after
Upload fileUploadUploads orupdatereplacesaone filefor an app. Mindin theper-app file cap before a bulkapp'simport:draft (30 files on free, 150 on Pro, 25 MBper file. Pass text files (HTML, CSS, JS, TS, JSON, SVG) as raw UTF-8 text with encoding="utf8" (the default) — do NOT base64-encode them, even if they contain quotes, backticks, ${...} or newlines; send the source verbatim. For binary files (images, fonts, audio, video) you have TWO options: (a) pass a public URL via `contentUrl` — Neblla downloads the bytes server-side (recommended: no need to spend tokens base64-encoding the file in this conversation), OR (b) pass a base64 string via `content` with encoding="base64". Always prefer `contentUrl` when you have one. Uploading the same path again overwrites the previous version. The file waits in the app's DRAFT (nobody sees it) until neblla_promote({ action: "publish" }) sends the whole draft at once to the app's environment (pro, the users', unless the developer chose pre): upload every file of a change, then publish once, and always publish before you finish. An app linked to its own repo (neblla_git) takes
before
—after
Validate requirementsWalk the SAME decision mapLists theNeblla dashboard wizard uses, before creating an app. STRONGLY RECOMMENDED before neblla_create_app on any new-app requestconfiguration(skipdecisionsitstillonlypending foredits/fixes to existing code). It front-loads every structural decision so the app is born configured right. How to use it: pass the `decisions` you can already answer from the user's request. The tool returns every still-pending decision, each with `how_to_infer` (how to answer it YOURSELF from context) and `ask_user` (whether it deservesaquestion to the human). The contract: ANSWER EVERY DECISION YOU CAN YOURSELF — only surface the ones in `ask_user_questions` to the user, and only when you genuinely cannot infer them; ask them all in ONE message. Then call this tool again with the full set. When `have_enough_context` is true, pass the returned `decisions` straight to neblla_create_app({ name, decisions }) — it scaffolds the matching pattern + themed UI pieces and writes the app config in one go. `plan` previews exactly what that will build. Decision ids: kind (game|app), players (single|lobby-only|multi — single = one device; lobby-only = everyone in one shared space with no separate rooms; multi = separate rooms, born
before
—after
VersionsThe version history of an app, with a branch per environment: "pre" (every save burst is a commit) and "pro" (each neblla_promote is a merge of pre into it), so this is howPuts anapp is rolled back. Without its own repo, Neblla keeps that git for it; linked to its own GitHub repo (neblla_git), the history is that repo's pre and pro branches, read and written with the developer's token, and a restore is a new commit on pre's branch. Actions: "history" (branch?) — the versions, newest first; "files" (commit, branch?) — what one version holds; "restore" (commit, branch?) — put theapp's pre back tothat version, from either branch (the users get it with neblla_promote). "migrate"an(url,earliertoBranch?version,toProBranch?) —ormovemoves
Alerts (read) — Lists the app's alerts, as its panel shows them, and their settings. More: neblla_get_llms tools/alerts_read
Assets — Searches Mundo's catalogue of 3D pieces and checks models and obbies. More: neblla_get_llms tools/assets
Auth secret (read) — Returns whether the app's secret login key exists (for apps with their own users). More: neblla_get_llms tools/auth_secret_read
Backups (read) — Lists the backups Neblla keeps of an app's data and their state. More: neblla_get_llms tools/backups_read
Ban settings read — Returns the messages a banned and a terminated user see in the app. More: neblla_get_llms tools/ban_settings_read
Billing read — Returns the automatic recharge state of an app's quota pool. More: neblla_get_llms tools/billing_read
Booking (read) — Reads the bookings side of a business: repetitions, free places, who booked, free times and session chats. More: neblla_get_llms tools/booking_read
Broadcast (read) — Estimates what a message to all users costs and shows how past sends went. More: neblla_get_llms tools/broadcast_read
Business (read) — Reads the places, people, agenda, notes, closures, applications and sellers of a business. More: neblla_get_llms tools/business_read
Byodb (read) — Reads the state of the app's own database (bring-your-own-database) and tests a connection. More: neblla_get_llms tools/byodb_read
Byos (read) — Returns the state of the developer's own server on their Cloudflare account. More: neblla_get_llms tools/byos_read
Check slug — Tells whether a slug is valid and free for the app; changes nothing. More: neblla_get_llms tools/check_slug
Community cases (read) — Lists and shows the formal notices filed about the app's community entries from outside it. More: neblla_get_llms tools/community_cases_read
Credits (read) — Returns the credit balance of a user in a credit kind. More: neblla_get_llms tools/credits_read
Developer notices (read) — Lists the notices Neblla sends to every developer, such as a change of sub-processors. More: neblla_get_llms tools/developer_notices_read
Developer profile update — Changes the developer's name, language and studio page. More: neblla_get_llms tools/developer_profile_update
Domains (read) — Reads the developer's own domains on their Cloudflare, their registration and mail forwarding. More: neblla_get_llms tools/domains_read
Git (read) — Reads how an app's files relate to a GitHub repo: repos, branches and link status. More: neblla_get_llms tools/git_read
Group (read) — Lists the groups of users (a team, a client) and the groups of one user. More: neblla_get_llms tools/group_read
Mark communication read — Marks a communication as read or unread. The user sees it live. More: neblla_get_llms tools/mark_communication_read
Matches (read) — Reads match results: disputes to settle, one player's tally and declared cheaters. More: neblla_get_llms tools/matches_read
Media (read) — Reads the media of an app (photos, sounds, videos, 3D models): collections, items and who holds a file. More: neblla_get_llms tools/media_read
Orders (read) — Lists the orders of the app's physical products and returns one. More: neblla_get_llms tools/orders_read
Payments account (read) — Returns the state of the developer's Stripe payout account for selling in their apps. More: neblla_get_llms tools/payments_account_read
Pre access (read) — Lists who may open the app's private pre version. More: neblla_get_llms tools/pre_access_read
Pre data read — Returns which data the app's pre plays with: its own or pro's. More: neblla_get_llms tools/pre_data_read
Pre stripe test read — Returns whether the app's pre sells through Stripe's test mode. Answers testMode separate, same or off. More: neblla_get_llms tools/pre_stripe_test_read
Projects (read) — Lists the developer's projects and the steps of the Cloudflare connection. More: neblla_get_llms tools/projects_read
Promote (read) — Shows where an app stands between its draft, pre and pro. More: neblla_get_llms tools/promote_read
Publish store (read) — Returns the state of the app's build for the stores with the developer's own store account. More: neblla_get_llms tools/publish_store_read
Sales (read) — Lists the app's sales and returns the receipt or invoices of one. More: neblla_get_llms tools/sales_read
Sample users (read) — Lists the sample users of an app: invented clients so the back office looks full. More: neblla_get_llms tools/sample_users_read
Scores (read) — Returns the top of a game ranking and one player's standing. More: neblla_get_llms tools/scores_read
Server key (read) — Returns whether the app's server key exists (for running room.js on the developer's own server). More: neblla_get_llms tools/server_key_read
Store — Creates, publishes and withdraws the 3D things the developer sells in Neblla's store, and reports. publish needs a passing file check, a price, the payout account and an adult seller. More: neblla_get_llms tools/store
Store (read) — Reads the developer's part of Neblla's store: their products, checks, fees and sanctions. More: neblla_get_llms tools/store_read
Tax (read) — Reads the developer's tax settings for sales to users, with Stripe Tax. More: neblla_get_llms tools/tax_read
Team (read) — Reads the developer's team for an app: members, friends and groups. More: neblla_get_llms tools/team_read
Trades (read) — Lists the trades between the app's users. More: neblla_get_llms tools/trades_read
Update app auth — Changes sign-in and access in an app: the login options, roles, user groups and the sentences shown on the consent screen. More: neblla_get_llms tools/update_app_auth
Update app business — Changes the business template of an app: booking sets, services, credits, standing places, sellers and reminders. More: neblla_get_llms tools/update_app_business
Update app community — Changes how user-published entries work in an app: who publishes, review mode, limits, categories and the AI pre-screen. More: neblla_get_llms tools/update_app_community
Update app games — Changes the game mechanics of an app: ranking boards, match results, fair draws and trades between users. More: neblla_get_llms tools/update_app_games
Update app inbox — Changes the inbox of an app: its cap per user, reply address, welcome message and the developer's saved replies. More: neblla_get_llms tools/update_app_inbox
Update app integrations — Changes an app's AI proxy (neblla.ai) and its outgoing webhooks. More: neblla_get_llms tools/update_app_integrations
Update app payments — Changes how purchases work in an app: the email checkout and what a purchase raises per product. More: neblla_get_llms tools/update_app_payments
Update app rooms — Changes how an app's rooms work: rooms policy, matchmaking, where the server code runs, Fog, P2P and connection settings. More: neblla_get_llms tools/update_app_rooms
Update app storage — Changes how an app keeps data: its tables, the defaults of user saves and what other users may see of a user's saves. More: neblla_get_llms tools/update_app_storage
Versions (read) — Returns the version history of an app (a branch per environment) and the files at a version. More: neblla_get_llms tools/versions_read
Mark a specific communication as read or unread (from the developer's side). The end user is notified live via a `communicationRead` socket event if connected.
Changed the definition of "neblla_business", "neblla_create_app", "neblla_create_product" and 3 more
⟨687 unchanged words⟩ it needs in its set (neblla_update_app business.booking). EACH SELLER PAID WHAT IS THEIRS (C400): an app with many sellers (the clubs of a padel app, the artists of a gallery, the authors of a shop, the teachers of a platform) charges each sale on the seller's OWN Stripe account, which pays its refunds and disputes (never Neblla nor the developer); the developer takes a commission, neblla_update_app({ business: { sellers: { fee: { percent?, fixed? (cents) } } } }), paid to their payout account once Stripe makes it available. A product names its seller (neblla_create_product seller), and a service paid at booking says seller: "site" (the site of each session: the club whose court it is) or one seller, in its set. "sellers" (each with status not_connected | pending | restricted | active, ready, requirementsDue; and the commission), "addSeller" { seller, name? } (a site of the business: "site:<id>", or a user of the app: "user:<id>"), "removeSeller" { seller } (their Stripe stays theirs), "sellerStatus" { seller } (asks Stripe again), "sellerLink" { seller } (Stripe's onboarding, for the seller to open right now). The seller links it themselves from the app too: neblla.business.sellers.mine() and neblla.business.sellers.connect({ seller }) (a user seller is themself; a site, the user whose application made it). Until a seller's account is active, what they sell waits (seller_waiting). Reselling between users is not there.
Changed the definition of "neblla_create_hook" and "neblla_sample_users"
⟨51 unchanged words⟩ the call (?user=<their id in the app>)., or, with to: "inbox", leaves it as a notice in the app's Buzón (People › Inbox) even when nobody is connected: the hook's name as sender, a readable line of the body (its message/text/title, else its first values) and the body as data; it cannot be answered, and past the inbox cap per sender the oldest notice goes. Needs the inbox module. To get an event of the app itself there (a purchase, a refund…), make an API secret with this URL as base and point an outgoing webhook at it. The URL carries a token shown only now; ⟨34 unchanged words⟩ delivered }, 404 hook_not_found, 409 room_not_live /user_not_online.user_not_online / feature_disabled (to: "inbox" with the inbox module off). No server of yours needed.
⟨44 unchanged words⟩ Changed the definition of "neblla_booking", "neblla_business", "neblla_credits" and 9 more
⟨46 unchanged words⟩ \"pre\" (its test copy: pre'sservices andconfig, on pre's data, its own unless ⟨215 unchanged words⟩
⟨84 unchanged words⟩ \"pre\" (its test copy: pre'sservices andconfig, on pre's data, its own unless ⟨186 unchanged words⟩
Added "neblla_draws" and "neblla_matches"; changed the definition of "neblla_booking", "neblla_business", "neblla_create_product" and 5 more (116 tools before, 118 now)
⟨274 unchanged words⟩ its buyer in every case of the periodpaidpaid; several ids offer several timetables in one product, and the client picks one). "changeStanding" { set, user, productId, series, when? } moves a client's standing place to another of its repetitions with no payment: when "now" (default; a place in every case left of the period, or series_full with the full ones) or "next" (from the next period). A service with chat: true gives ⟨326 unchanged words⟩
⟨16 unchanged words⟩ ,"cancelSession","times","chat","chatPost","changeStanding"],"type":"string"},"appId":{ ⟨88 unchanged words⟩ "},{"type":"null"}]},"productId":{"type":"string"},"reason":{"type":"string"},"refund"
Changed the definition of "neblla_booking", "neblla_business", "neblla_copy_app_config" and 18 more
⟨35 unchanged words⟩ {"type":"string"},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (itsdrafttest copy: pre's services and config, on pre's data, its own unless it plays with pro's).TryLeft out: where thebusinessappinis,preitsbeforechosenpublishingenvironment
Added "neblla_sample_users" (115 tools before, 116 now)
Sample users of an app: invented clients, players or members so its back office looks full and real without showing anyone real (the students of a yoga studio, the clients of a dog groomer, the players of a ranking): an example app's screenshots, or a studio showing its client how the app will look with people in it, best in its pre (env "pre"). Remove them all with removeAll before the app opens to the public. They have no account: nobody signs in as them and no mail is sent to them; their address ends in @ejemplo.neblla.app. They count in no quota, no statistic, no «write to everyone» and n…
Added "neblla_domains" and "neblla_orders"; changed the definition of "neblla_booking", "neblla_business", "neblla_byos" and 5 more (113 tools before, 115 now)
⟨318 unchanged words⟩ ), "chatPost" { set, slot, text, scope? } writes in it as thebusiness.business, also before anyone booked (a notice: whoever books reads it there); scope "following" or "all" writes it in those cases of the repetition (the ones made so far, a year ahead). A slot is a session of the business: ⟨177 unchanged words⟩ free time and others join, like padel), recoverStanding? (true: a case of a standing place that the business cancels or closes comes back as a credit; by default it does not, the fee is the same) }. "slots" also lists the sessions the business cancelled (state "cancelled", with its reason); days closed for a site, a resource, a professional or everything: neblla_business "close". Users book with neblla.business.booking.
Added "neblla_media" and "neblla_projects"; changed the definition of "neblla_billing", "neblla_byos", "neblla_create_app" and 10 more (111 tools before, 113 now)
What the developer pays Nebllafor one app: the Pro subscription of a PROJECT (€10 a month perappproject, or $10 outside Europe, taxesincludedincluded; every app of a Pro project shares its own pool of 2000 credits a day and stops drawing from the account's free pool) and one-off quotapacks.packs (they go to the pool the project draws from now: the account's for a free project, the project's own while it is Pro). Name the project with projectId, or name an app (appId) and it is that app's project (neblla_projects "list" says which apps are where; most accounts only have Explorer). "pro_checkout" and "quota_pack_checkout" return a URL the ⟨44 unchanged words⟩ already paid (answers endsAt) and then theapp
Added "neblla_copy_community_entries" and "neblla_copy_files"; changed the definition of "neblla_booking", "neblla_business", "neblla_copy_app_config" and 8 more (109 tools before, 111 now)
⟨68 unchanged words⟩ : keeps the local hour when the clockschangechange; left out, the zone of its site or its hours, then its professional's hours, else UTC) } or an RRULE string ("FREQ=WEEKLY;BYDAY=TU, ⟨147 unchanged words⟩ of a service booked at a time). "series" { set } without an id lists the set's repetitions (their ids sell a standing place: business.standing { <productId>: { set, series } }, each purchase books its buyer in every case of the period paid). A service with chat: true gives each session a private chat of the people booked in it: "chat" { set, slot, before?, limit? } reads it (newest first; from: "business" or the client's user and name), "chatPost" { set, slot, text } writes in it as the business. A slot is a session of the business ⟨189 unchanged words⟩
⟨14 unchanged words⟩ Changed the definition of "neblla_business", "neblla_list_apps", "neblla_team" and 1 more
⟨219 unchanged words⟩ who? }, "removeNote" { id }. A client's card: "userBookings" { user } (the user id in the app: their bookings of every service, upcoming and past), "userNotes" { user } (the business's private notes about them, never shown to them), "userNote" { user, text }, "removeUserNote" { id }. Sessions are made with neblla_booking createSlot (site, ⟨18 unchanged words⟩
⟨12 unchanged words⟩ ,"agenda","note","notes","removeNote","userBookings","userNotes","userNote","removeUserNote"],"type":"string"},"address":{ ⟨53 unchanged words⟩ "},{"type":"null"}]},"text":{"type":"string"},"timeZone":{"type":["string","null"]},"to":{"type":"string"},"user":{"description":"The user id in the app (userBookings, userNotes, userNote)","type":"string"},"who":{"type
Added "neblla_business"; changed the definition of "neblla_booking", "neblla_get_activity_log", "neblla_list_apps" and 2 more (108 tools before, 109 now)
⟨193 unchanged words⟩ "move" { set, booking, toSlot }, "cancelSession" { set, slot, reason? } (the whole session, everyone gets their credits or money back; it stays in the agenda as cancelled), "times" { set, from?, to?, site?, staff?, step? } (the free times of a service booked at a time). A slot is a session of the business: createSlot and updateSlot also take site?, staff? and resource? (ids from neblla_business) and min? (the fewest people for it to take place): it must fit the hours of its site, professional and resource, the professional must be free and the resource must have a unit free, or it answers "busy" with what is taken (for a repetition, every case is checked: all or none, or skipBusy: true leaves the busy dates out). Sets are configured with neblla_update_app({ business: { ⟨10 unchanged words⟩ with { cancelHours, pay: "booking", product }. A set is also a service: { name?, minutes?, min?, max?, confirmHours? (12 by default with a min: how long before the start the min must be reached, or the session is cancelled with everything given back), resources? ([ids], any of them), staff? (true or [ids]), steps? ([{ minutes, resources, staff? }], in a row: the bath, then the table), book? ("sessions": the business opens them, the default; "times": the client picks a free time and it is theirs alone; "open": the client picks a free time and others join, like padel) }. Users book with neblla.business.booking.
Changed the definition of "neblla_set_slug"
⟨17 unchanged words⟩ true` only tells whether it is valid andfree.free; when another app of YOURS holds it (its slug or an old one) it says so (`reason: "yours"`, `heldBy`). `slug: null` removes it. An old slug keeps ⟨11 unchanged words⟩ take it), and the users' sessions follow. `move: true` takes a slug from another app of yours (e.g. moving a game to a new app): the other app loses it and its address leads to this one from then on. At least 5 characters. Changing it changes the ⟨7 unchanged words⟩
⟨19 unchanged words⟩ change nothing","type":"boolean"},"move":{"description":"Take it from another app of yours that holds it (its slug or an old one)","type":"boolean"},"slug":{"description":"Wanted slug (lowercase
Added "neblla_alerts", "neblla_auth_secret", "neblla_backups" and 50 more; removed "neblla_draft" and "neblla_snapshot"; changed the definition of "neblla_archive_app", "neblla_ban_settings", "neblla_byodb" and 44 more (57 tools before, 108 now)
⟨5 unchanged words⟩ its data is bundled onto the CDN and the active records are cleared from Neblla's database. It stays in the ⟨16 unchanged words⟩ Neblla also does this on its own forlong-unusedfreeapps.apps with no developer or user activity for 30 days; apps with an active subscription are exempt.
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Added "neblla_archive_app", "neblla_ban_settings", "neblla_byodb" and 22 more; changed the definition of "neblla_create_product" and "neblla_update_app" (32 tools before, 57 now)
⟨10 unchanged words⟩ public ID","type":"string"},"currency":{"description":"ISO 4217 code, e.g. \"eur\" (default usd)","pattern":"^[a-zA-Z]{3}$","type":"string"},"defaultLang":{"enum":["en","es","fr","de","it","pt","ja","ko","zh","ar"],"type":"string"},"description":{"anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"Description, string or per-language object"},"image":{"description":"Product image URL", ⟨56 unchanged words⟩ — required for consumable","type":"number"},"published":{"description":"Buyable right away (default true; forced false until Stripe Connect is active)","type":"boolean"},"step":{"description":"Quantity step ⟨17 unchanged words⟩
Added "neblla_publish_community_entry"; changed the definition of "neblla_get_community_entry" (31 tools before, 32 now)
⟨6 unchanged words⟩ payload (the dev-defined JSON), whatever itsstatus.status, plus its user reports (open count, count by reason, and each report with an opaque reporter id). Stats carry favorites, rating and reviews.
Publish a community entry AS YOURSELF — owned by your own user in the app (the app user whose email is your developer email; log into the app once first). Use it to seed a community with starter content. It skips the review queue (you are a moderator of your app) and obeys the same rules as neblla.community.publish: community must be enabled, contentType/category must be allowed, payload ≤ maxPayloadBytes, no "$"/"." keys.
Added "neblla_get_community_entry", "neblla_list_community_entries" and "neblla_moderate_community_entry"; changed the definition of "neblla_get_llms" and "neblla_update_app" (28 tools before, 31 now)
⟨72 unchanged words⟩ ), social (friends + dev↔user communications), comunidad (user-published entries: neblla.community, moderation), pagos (products/Stripe), ia (neblla.ai routes), ⟨134 unchanged words⟩
⟨95 unchanged words⟩ salas","servidor","identidad","social","comunidad","pagos","ia","publicar","referencia"," ⟨5 unchanged words⟩
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Authorization not required, issuer https://developers.neblla.com
Certificate recorded, valid to 2026-12-10
Authorization not required, issuer https://developers.neblla.com
First tool surface recorded: 28 tools (server version 2.4.0+build.36)
Showing the latest 33 events. The API returns up to 500 and filters by kind: ?kind=tool_surface_changed
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["create","rotate","status","revoke"],"type":"string"}, ⟨13 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["list","restore","status"],"type":"string"},"appId":{ ⟨10 unchanged words⟩ restore: the day of the copy, YYYY-MM-DD, one of list's days","type":"string"},"env":{"description":"For restore and status: \"pro\" (default, the published app) or its \"pre\"","enum":["pro","pre"], ⟨7 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨7 unchanged words⟩ :"New message shown to banned users","maxLength":4000,"type":"string"},"terminatedMessage":{"description":"New message shown to terminated users","maxLength":4000,"type":"string"}},"required":[ ⟨3 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": cancel_pro {charges nothing}; resume_pro {charges nothing, so it needs no approval}; auto_recharge {appId, enabled, monthlyCap}","enum":["pro_checkout","quota_pack_checkout","cancel_pro", ⟨16 unchanged words⟩ ":{"description":"auto_recharge: on or off (required for auto_recharge)","type":"boolean"},"monthlyCap":{ ⟨4 unchanged words⟩ it may spend in a month, in euros(required to switch it on)","type":"number"},"projectId":{ ⟨16 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": following {this and the next}; all {every case not started yet}; cancel {set, booking, refund?}; move {set, booking, toSlot}; next {from the next period}","enum":["createSlot","updateSlot","deleteSlot","series","slots","list","cancel","move","cancelSession","times","chat","chatPost","changeStanding"],"type":"string"} ⟨26 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target,\"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type" ⟨17 unchanged words⟩ {"type":"string"},"refund":{"type":"boolean"},"repeat":{"anyOf":[{"typedescription":"string"},{"additionalProperties"cancel: false,"properties":{"byDay":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"byMonth":{"anyOf":[{"type":"integer"},{"items":{"type":"integer"},"type":"array"}]},"byMonthDay":{"anyOf":[{"type":"integer"},{"items":{"type":"integer"},"type":"array= no money back and it runs at once"}]},"count":{"type":"integerboolean"},"exceptrepeat":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"freq":{"enum":["daily","weekly","monthly","yearly"],"type":"string"},"interval":{"type":"integer"},"rrule":{"type":"string"},"timeZone":{"type":"string"},"until":{"type":"string"},"weekly":{"const":true,"type":"boolean"},"wkst":{"type":"string"}},"type":"object"}]},"resource":{ ⟨76 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["estimate","By action: send {same fields}; resume {id}","statusenum",:["listsend","resume"],"type":"string"}, ⟨12 unchanged words⟩ payload, as neblla_send_communication"},"env":{"default":"pro","description":"\"pro\" (the app's users, ⟨13 unchanged words⟩ "Only the members of this group (neblla_group); omit for every user","type":"string"},"id":{ ⟨8 unchanged words⟩ "description":"Plain-text message (estimate, send)","maxLength":5000,"type":"string"},"role":{" ⟨16 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"about":{"type":["string","null"]},"action":{"enumdescription":["list","By action: save"{kind,"id?, …fields}; remove"{kind,"accounts"id}; note {who,"agendaamount, date?, note?}; close {kind?, id?, from, to?, reason?, timeZone?}; reopen {closure}; approve {application}; reject {application, reason?}","noteenum",:["notessave","removeNoteremove","userBookingsnote","userNotesremoveNote","userNote","removeUserNote","close","closures","reopen","applications","approve","reject","rateUser","userRating","reviews","hideReview","sellers","addSeller","removeSeller","sellerStatus","sellerLink"],"type":"string"},"address":{"type":["string","null"]},"amount":{"description":"note: cents the business owes them (negative: paid); only a record","type":"integer"},"appId":{"type" ⟨42 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target,\"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"fields":{"anyOf":[{"additionalProperties":{},"type":"object"},{"type":"null" ⟨9 unchanged words⟩ (true) or the shown (false); hideReview: false shows it again","type":"boolean"},"hours":{ ⟨37 unchanged words⟩ ":{"description":"close: what clients read,(«Festivo»)e.g. \"Public holiday\"","type":"string"},"review":{ ⟨86 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["statusBy action: cancel {only before the last phase}","validateenum",:["migrate","to_neblla","cancel"],"type" ⟨17 unchanged words⟩ : the connection settings of the engine","properties":{},"type":"object"},"engine":{"description" ⟨36 unchanged words⟩ ": take pre back to the app's database(no engine or config)","type":"boolean"},"fresh":{"description":"For to_neblla, only when their database does not answer: start from scratch on Neblla's store (their data stays behind, the retry queue is discarded)","type":"boolean"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": set {provider: \"cloudflare\"}","enum":["set","status","remove"],"type":"string"}, ⟨18 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["list","get","close"],"type":"string"},"appId" ⟨20 unchanged words⟩ \"close\": why, for the record","maxLength":4000,"type":"string"},"outcome":{" ⟨28 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"amount":{"description":"Units to consume (default 1)","minimum":1,"type":"integer"},"appId":{" ⟨35 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"Target ⟨22 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"Target ⟨15 unchanged words⟩ )","items":{"type":"string"},"maxItems":500,"type":"array"},"fromAppId":{" ⟨10 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"Target ⟨22 unchanged words⟩ short note for the app history (neblla_versions), e.g. \"Files from the old app\"","maxLength":120,"type":"string"},"paths":{"description" ⟨9 unchanged words⟩ )","items":{"type":"string"},"maxItems":500,"type":"array"}},"required":[ ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"acceptDpa":{"description":"The developer accepts Neblla's terms of use and data processing agreement (DPA), together. Asked once, before their first app (and again when its version changes): the call answers error \"dpa_required\" with the links — showtruethemonlytoafter the developerand, only if THEY accept, call again with acceptDpa: true. Never accept onacceptstheirthebehalf.terms","type":"boolean"},"decisions":{"additionalProperties":{},"description":"The COMPLETE decision set from neblla_validate_requirements (have_enough_context: true → pass its `decisions` verbatim). Scaffolds the matching pattern with the themed UI pieces for each enabled feature AND writes the app config (its neblla.* modules, connections, serverMode, rooms policy, matchmaking mode, AI payer, theme). The recommended way to create any non-blank app.","type":"object"},"name":{"description":"Display name for the app","minLength":1,"type":"string"},"projectId":{"description":"The project the app goes to (neblla_projects list). Omitted: the account's Explorer, where apps land by default.","type":"string"},"template":{ ⟨5 unchanged words⟩ scaffold the canonical files of this page (rooms: the one room example, where the server sets the state and the clients paint it and talk through message; lobby-only; solo-save; home), with no feature pieces and no theme, and the modules of that page. Prefer `decisions`. Omit both for a blank app: only auth and db, the rest with neblla_update_app({ features }).","type":"string"}},"required": ⟨3 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩ a letter, then letters, digits, _ or- (≤ 40)","type":"string"},"room":{ ⟨19 unchanged words⟩ file), the user named in each call, or the app's Buzón","enum":["room","user"," ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨12 unchanged words⟩ beside its files (one_time: for good; subscription: while it lives): a link (a Canva template, a hidden video, a Discord invite) or a short text (a code, instructions). Never in the public catalogue; the buyer reads it with neblla.media.mine() (`content`). Neblla controls who sees it, not whether the buyer passes it on","items":{"anyOf":[{"additionalProperties":false,"properties":{"label":{"anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"What the button says, e.g. \"Abrir en Canva\" or{es, en }"},"type":{"const":"link","type":"string"},"url":{"description":"http(s) address","type":"string"}},"required":["type","label","url"],"type":"object"},{"additionalProperties":false,"properties":{"label":{"anyOf":[{"$ref":"#/properties/content/items/anyOf/0/properties/label/anyOf/0"},{"$ref":"#/properties/content/items/anyOf/0/properties/label/anyOf/1"}]},"text":{"description":"Up to 2000 characters: a code, access instructions","type":"string"},"type":{"const":"text","type":"string"}},"required":["type","text"],"type":"object"}]},"maxItems":10,"type":"array"},"currency":{"description":"ISO 4217 code Stripe charges in, e.g. \"usd\" (default eur). Prices are in its smallest unit: cents for most, whole units for zero-decimal ones (jpy, krw…), thousandths ending in 0 for bhd, jod, kwd, omr, tnd; a price under Stripe's minimum charge in that currency (0.50 EUR/USD…) is refused","pattern":"^[a-zA-Z]{3}$","type":"string"},"defaultLang":{"enum" ⟨13 unchanged words⟩ anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"Description, string or ⟨38 unchanged words⟩ anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"Product name, string ⟨9 unchanged words⟩ ":"Price in cents — required for one_time, and per period for subscription","type":"number"},"pricePerUnit":{ ⟨13 unchanged words⟩ "description":"Buyable right away (default true). Without an active payout account it waits, hidden, and publishes by itself once the account is active (connectUrl opens the onboarding).","type":"boolean"},"seller":{"description":"Who is paid for it(C400): \"site:<id>\" or \"user:<id>\", one of the app's sellers(made one if new); omit for the app itself","type":"string"},"shipping":{"additionalProperties":false,"description":"Physical product (one_time or consumable): where it ships and the postage of each zone","properties":{"zones":{"items":{"additionalProperties":false,"properties":{"amount":{"description":"Postage in cents (the product's currency); 0 is free","minimum":0,"type":"integer"},"countries":{"description":"ISO 3166-1 alpha-2 codes, e.g. [\"ES\"]","items":{"type":"string"},"type":"array"},"name":{"description":"What the buyer sees at the checkout, e.g. \"España\" or \"Resto de Europa\"","type":"string"}},"required":["name","countries","amount"],"type":"object"},"maxItems":20,"minItems":1,"type":"array"}},"required":["zones"],"type":"object"},"step":{"description":"Quantity step for consumable ⟨2 unchanged words⟩ number"},"stock":{"anyOf":[{"minimum":0,"type":"integer"},{"type":"null" ⟨2 unchanged words⟩ for sale in all (one_time and consumable); omit for no limit"},"stockRefundReturns":{"description":"Whether a refunded unit goes back on sale (defaulttrue; false for e.g. a numbered, signed edition)true","type":"boolean"},"trialDays":{"description":"subscription: free trial days (optional)","maximum":730,"minimum":0,"type":"integer"},"type":{" ⟨12 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": adjust {owner, kind?, delta}","enum":["balance","adjust"],"type":"string"},"appId":{"type":"string"},"delta":{"description":"adjust: credits to add, negative to take","type":"integer"},"env":{"description" ⟨9 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target,\"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"kind":{"description":"The credit kind (default \"default\")","type":"string"},"owner":{"description ⟨14 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"actionflag_others moves the flag from the odd Fog to the majority; annul clears it":{,"enum":["flag_others","annul"],"type ⟨23 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","properties":{},"type":"object"}
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨17 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨21 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["add ⟨5 unchanged words⟩ description":"The address, for add and resend_confirmation","format":"email","type":"string"}},"required": ⟨3 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["list","dismiss"],"type":"string"},"id" ⟨2 unchanged words⟩ notice to dismiss","type":"string"}},"required":["action"],"type":"object"}
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"about":{"anyOf":[{"maxLength":1500,"type":"string"},{"type":"null"}],"description":"The studio's presentation"},"lang":{"description":"Dashboard language","enum":["en","es","fr","de","it","pt","ja","ko","zh","ar"],"type":"string"},"links":{"description":"Other links (social, stores…)","items":{"additionalProperties":false,"properties":{"label":{"maxLength":40,"type":"string"},"url":{"type":"string"}},"required":["url"],"type":"object"},"maxItems":6,"type":"array"},"name":{"description":"New public name: what users see as the author of the developer's apps (e.g. a studio or company name). The email is never shown.","maxLength":50,"type":"string"},"studioSlug":{"description":"The studio's short name: its page play.neblla.com/studio/<studioSlug>","type":"string"},"web":{"description":"The studio's official web (https)","type":["string","null"]}},"type":"object"}
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"accept":{"description":"copy: ⟨9 unchanged words⟩ ,"type":"boolean"},"action":{"enumdescription":["listBy action: add {domain}; copy {domain}; remove {domain}; point_web {domain, appId, confirm?}; unpoint_web {domain}; transfer {domain}; register {domain}; mail_forward {domain, to, from?}; mail_stop {domain, from?}","addenum",:["copyadd","checkcopy","remove","point_web","unpoint_web","registration","transfer_check","transfer","check_new","register","mail_status","mail_forward","mail_stop","mail_restore"],"type": ⟨10 unchanged words⟩ string"},"confirm":{"description":"point_web/ registeronly: true applies thedeveloperchangeagreedto the web records.","type":"boolean"},"domain":{ ⟨39 unchanged words⟩ "},"replaceMailboxes":{"description":"mail_forward:replacetrue replaces the other provider's mailboxes(the developer agreed)","type":"boolean"},"to":{ ⟨11 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": info {draw}; opens {user?, draw?, limit?}","enum":["info","opens"],"type" ⟨18 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"], ⟨13 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨5 unchanged words⟩ baseVersion":{"description":"The `version` neblla_get_file returned: the edit fails if the file changed since","type":"string"},"edits":{ ⟨3 unchanged words⟩ order, all or none","items":{"additionalProperties":false,"properties":{"newText":{"description":"What goes in its place (\"\" deletes it)","type":"string"},"oldText":{"description":"The exact text to replace (whitespace included)","minLength":1,"type":"string"},"replaceAll":{"description":"Replace every place oldText appears, instead of requiring exactly one","type":"boolean"}},"required":["oldText","newText"],"type":"object"},"maxItems":100,"minItems":1,"type":"array"},"message":{"description":"A short note about thischange; it names this version in neblla_versions","maxLengthchange":120,"type":"string"},"path":{" ⟨14 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨34 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"after":{"description":"The ⟨14 unchanged words⟩ ,"type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":200,"maximum":500,"minimum":1,"type":"integer"}},"required":[" ⟨3 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨24 unchanged words⟩ , for the moderation log and the webhook","maxLength":200,"type":"string"},"userId":{" ⟨13 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨25 unchanged words⟩ of the previous page (or an ISO date: lines before it)","type":"string"},"by":{ ⟨10 unchanged words⟩ :"Lines per page (default 50)","maximum":200,"minimum":1,"type":"integer"},"moderation":{"description" ⟨16 unchanged words⟩ here: panel, mcp (you) or app(the moderators)","enum":["panel","mcp"," ⟨7 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨10 unchanged words⟩ ,"type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"], ⟨6 unchanged words⟩ in this language when the entry has it","maxLength":16,"type":"string"}},"required":[ ⟨4 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"maxLength":{"default":20000,"description":"Maximum length of `content` to return in thischunk. The default (20000) stays comfortably under the response-size limit of MCP clients; lower it further if your client still truncates the reply.","maximum":65536,"minimumchunk":1024,"type":"integer"},"offset":{"default":0,"description":"Where to start reading, in units of the returned `content` (characters for utf8 text, base64 characters for binary). 0 = start of file. Pass the previous response's `nextOffset` to get the next chunk.","minimum":0,"type":"integer"},"path":{" ⟨13 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"An app's id: the guide cut to that app's modules(chapters and sections of modules it does not have are left out).","type":"string"},"group":{ ⟨11 unchanged words⟩ ,"type":"string"},"lang":{"default":"es","description":"Language. The guide is written in Spanish and most chapters are also translated. \"en\" serves the English version of a section when one exists and falls back to the Spanish original otherwise, saying which in `langNote` — so it is always safe to ask for \"en\".Language","enum":["es","en"],"type":"string"},"maxLength":{"default":20000,"description":"Maximum length of `guide` to return in thischunk. The default stays under the response-size limit of MCP clients.","maximum":65536,"minimumchunk":1024,"type":"integer"},"offset":{"default":0,"description":"Character position to start reading from. 0 =start. Pass the previous response's `nextOffset` for the next chunk.","minimumstart":0,"type":"integer"},"role":{" ⟨11 unchanged words⟩ ,"type":"string"},"section":{"defaultanyOf":[{"enum":["index","full","access","empezar","core","salas","servidor","identidad","social","comunidad","pagos","ia","mundo","sonido","creator","publicar","referencia","mcp"],"type":"string"},{"type":"string"}],"description":"Which part of the guide to read.Start with \"empezar\" (whether Neblla fits and what shape to give the app), thenDefault \"core\"— the mandatory contract, and the default.. \"index\" =table ofcontents, \"full\" =the whole guide.everything, \"access\" (with appId) = what each role andteamgroupof that appcanuse."use, \"enumtools/<name>\" = one tool's reference. Chapters:["index","full","access","empezar","(when to use neblla); core","(the base contract); salas","(rooms and matchmaking); servidor","(server (fog and no-code)); identidad","(identity and persistence); social","(friends and inbox); comunidad","(community); pagos","(purchases); ia","(built-in ai); mundo (the 3d engine); sonido (eco and sound in your apps); creator (the creation portal and the store); publicar","(publishing and quotas); referencia","(sdk reference); mcp"],"type":"string(mcp server)"}},"type":"object"}
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"after":{"description":"The `now` of an earlier answer (or an ISO date): only what happened since","type":"string"},"appId":{ ⟨65 unchanged words⟩ description":"Lines per page (default 50)","maximum":200,"minimum":1,"type":"integer"},"room":{" ⟨13 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨25 unchanged words⟩ :"Lines per page (default 100)","maximum":500,"minimum":1,"type":"integer"},"offset":{"minimum":0,"type":"integer"}},"required":[" ⟨3 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨7 unchanged words⟩ :"How many days (default 30)","maximum":90,"minimum":7,"type":"integer"},"format":{"description" ⟨13 unchanged words⟩ time zone for the classes' times (defaultUTC; the days areUTC)","type":"string"}},"required": ⟨3 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"], ⟨3 unchanged words⟩ "description":"The user's mongo ObjectId hex string(the `_id` returned by neblla_list_users)","type":"string"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["repos","branchessync pulls into pre and deletes removed files; example publishes pro to the public repo","statusenum",:["link","sync","commit","set_mode", ⟨24 unchanged words⟩ pre's branch, the one to follow (default: the repo default)","type":"string"},"deletes":{ ⟨25 unchanged words⟩ all in one commit","items":{"additionalProperties":false,"properties":{"content":{"description":"The new contents, as text","type":"string"},"path":{"description":"Path as the app knows it (\"index.html\"); the repo output folder is prepended","type":"string"}},"required":["path","content"],"type":"object"},"type":"array"},"message":{"description":"For \"commit\": the commit message","maxLength":200,"type":"string"},"proBranch":{"description" ⟨21 unchanged words⟩ that holds the app (default \"dist\"; \"\" = repo root)","type":"string"},"syncMode":{" ⟨11 unchanged words⟩ ":{"description":"For link: GitHub token, only if the developer has not signed in with GitHub (stored encrypted, never returned)","type":"string"},"url":{ ⟨13 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": create {name}; delete {name}; add {group, user, role}; remove {group, user}","enum":["create","delete","add","remove","list","of"],"type":"string"},"appId":{ ⟨13 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"], ⟨19 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","properties":{},"type":"object"}
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨5 unchanged words⟩ emailFilter":{"description":"Case-insensitive email substring filter, at least 3 characters","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"fromFilter":{"default":"all","description":"\"user\" = sent by end-user, \"dev\" = sent by you, \"all\" = both","enum":["all","user","dev"],"type":"string"},"importantOnly":{"default":false,"type":"boolean"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"},"readFilter":{"default":"all","enum":["all","read","unread"],"type":"string"},"skip":{"default":0,"minimum":0,"type":"integer"},"sort":{"default":"date_desc","enum":["date_desc","date_asc","email_asc" ⟨7 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨14 unchanged words⟩ ,"type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":20,"maximum":50,"minimum":1,"type":"integer"},"ownerId":{"description":"Filter byauthor. This is the SAME per-app user id that neblla_list_users and neblla_search_users_by_email return (entry.owner.id is that id), so you can go straight from a user to everything they published — no name matching needed.author","type":"string"},"q":{ ⟨16 unchanged words⟩ most serious first by default (each carries `priority`: notice > serious > reports > prescreen > review); newest/oldest for plain date order; votes: most voted first (stats.score = votes for minus votes against)","enum":["gravity","newest"," ⟨13 unchanged words⟩ pending (awaits approval), rejected, removed, suspended(a formal notice is open), private (only its author sees it)","enum":["published","pending"," ⟨10 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨21 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"limit":{"maximum":200,"minimum":1,"type":"integer"},"status":{"enum ⟨10 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨9 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨4 unchanged words⟩ :"string"},"emailFilter":{"description":"Substring matchPartagainstof the user's email(case-insensitive).,IgnoredatifleastwithCommunicationsOnly3ischaracterstrue.(case-insensitive)","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":50,"maximum":100,"minimum":1,"type":"integer"},"skip":{"default":0,"minimum":0,"type":"integer"},"sort":{"default":"date_desc","enum":["date_desc","date_asc","email_asc","email_desc"],"type":"string"},"withCommunicationsOnly":{"default":false,"description":"Only return users with a non-empty ⟨8 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": resolve {match, winner: \"enumA\":[| \"disputesB\",| \"playerdraw\","cheatersnote?}; annul {match, note?}","enum":["resolve","annul","declareCheater","clearCheater"], ⟨16 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target,\"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type" ⟨13 unchanged words⟩ ","type":"string"},"note":{"maxLength":500,"type":"string"},"user":{"description ⟨20 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["collections"By action: shrink {ids | collection,"listmaxSide?, dryRun?}; update {id, title?, text?, alt?, note?, fields?, replaceFields?}; move {ids, collection, position?}; reorder {collection, ids}; delete {ids}; create_collection {collection}; rename_collection {collection, newName}; delete_collection {collection, withItems?}; private {ids |…","getenum",:["upload","update","move","reorder", ⟨4 unchanged words⟩ ,"delete_collection","private","link","unlink","holders","download_link"],"type":"string"},"alt":{ ⟨37 unchanged words⟩ ,"type":"boolean"},"fields":{"additionalProperties":{"type":["string","number","boolean","null"]},"description":"For update: merged into the card; ⟨33 unchanged words⟩ "description":"For upload","items":{"additionalProperties":false,"properties":{"alt":{"description":"What the image shows, for screen readers and search engines","type":"stringobject"},"content":{"description":"The file as base64 (or a data: URI), when there is no URL. The whole call is limited to ~8 MB, so a URL for anything big","type":"stringarray"},"contentUrllimit":{"description":"Public URL of the file: Neblla downloads it (preferred: e.g. the original of a photo on the old site)","type":"stringinteger"},"fields":{"additionalPropertiesmaxSide":{"type":["string","number","boolean"]},"description":"The developer's own fields: { year: 2023, size: \"70 x 45 cm\",Forpriceshrink:520, available:thetruelong}","type":"object"},"name":{"description":"Thesideoriginaltofilereducenameto,kept forinreferencepx","type":"stringinteger"},"textnewName":{"description":"A longer text: a caption, aFordescription","type":"stringrename_collection"},"title":{"type":"string"}},"type":"object"},"type":"array"},"limit":{"type":"integer"},"maxSidenote":{"description":"Forshrinkupdate: thelong side to reduce to,noteinforpxthe(defaultdeveloper's2560)AI","type":"integer"},"newName":{"description":["For rename_collectionstring","type":"stringnull"]},"offset":{"type":"integer"},"position":{"description":"For upload and move: at the end(default) or the start of the collection","enum":["end","start"], ⟨6 unchanged words⟩ (required) and upload: the original is private, unlocked by a purchase","type":"boolean"},"productId":{ ⟨3 unchanged words⟩ and unlink: a one_time product or a subscriptionof the app","type":"string"},"publicWidth":{ ⟨7 unchanged words⟩ variant the public sees of a private image, in px (default 1280)","type":"integer"},"replaceFields":{ ⟨22 unchanged words⟩ reduces the images bigger than a screen needs, a number to that long side in px; only when the developer asked"},"text":{"type":["string" ⟨20 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["approve ⟨23 unchanged words⟩ :"Plain-text explanation the author will read","maxLength":1000,"type":"string"},"reason":{"description":"Required for reject, remove and suspend; the author reads it","enum":["copyright","inappropriate","hate" ⟨13 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["listBy action: update {orderId, status?, carrier?, tracking?, trackingUrl?}","getenum",:["update"],"type":"string"},"appId ⟨6 unchanged words⟩ string"},"carrier":{"anyOf":[{"maxLength":60,"type":"string"},{"type":"null" ⟨2 unchanged words⟩ e.g. \"Correos\""},"env":{"default":"pro","description":"\"pro\" (the app's users, ⟨8 unchanged words⟩ ,"pre"],"type":"string"},"limit":{"maximum":200,"minimum":1,"type":"integer"},"orderId":{"description":"The order's id from \"list\" (get, update)","type":"string"},"skip":{"minimum":0,"type":"integer"},"status":{"description":"list: only these; ⟨10 unchanged words⟩ string"},"tracking":{"anyOf":[{"maxLength":100,"type":"string"},{"type":"null" ⟨4 unchanged words⟩ number"},"trackingUrl":{"anyOf":[{"maxLength":500,"type":"string"},{"type":"null ⟨14 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": set_descriptor {prefix}","enum":["status","onboarding_link","dashboard_link","set_descriptor","disconnect"], ⟨3 unchanged words⟩ {"description":"Required true for \"disconnect\".","type":"boolean"},"prefix":{ ⟨14 unchanged words⟩ "description":"A project with its own Stripe(C220): its payoutaccount. Omitted, or a project using the account's: the developer's.account","type":"string"}},"required": ⟨3 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": add {email, expiresAt?}; remove {email}; link {password?, expiresAt?, label?}; revoke {linkId}","enum":["list","add","remove","link","revoke"," ⟨34 unchanged words⟩ date after which it no longer opens pre(omit for no expiry)","type":"string"},"label":{"description":"For \"link\": a name for it, such as who it was sent to","type":"string"},"linkId":{ ⟨11 unchanged words⟩ :"string"},"password":{"description":"For \"link\"link:6 to 128returnedcharacters;onceomitiftogenerated;getnoaemailgeneratedisonesent","type":"string"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨4 unchanged words⟩ :"string"},"on":{"description":"Settrue:it;testomitmode;tofalse:readrealitcharges","type":"boolean"}},"required":["appId","on"],"type":"object"}
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": create {name}; rename {projectId, name}; delete {projectId}; move {appId, projectId, confirm?}; set_app_caps {appId, credits?, store?, files?}; cloudflare_topic {projectId?, topic, on?}; remove_cloudflare {projectId?, force?}; connect_db {projectId?, engine, config, migrate?}; remove_db {projectId?}; use_own_stripe {projectId}…","enum":["list","create","rename","delete","move","set_app_caps","cloudflare_setup","cloudflare_topic","cloudflare_check","connect_cloudflare","remove_cloudflare","connect_db","remove_db"," ⟨11 unchanged words⟩ ,"type":"string"},"config":{"additionalProperties":{},"description":"connect_db: { uri, dbName? } (mongo), { url, authToken } (turso), { uri } (postgres, mysql)","type":"object"},"confirm":{ ⟨3 unchanged words⟩ once the developer agreed to move the app'sdata","type":"boolean"},"credits":{ ⟨69 unchanged words⟩ ":"The project (list gives the ids). Omitted in connect_*/remove_*: the account's.","type":"string"},"store":{ ⟨21 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"to_pre only sets the target to pre, sends nothing; promote sends pre to pro. By action": rollback {releaseId}","enum":["status","publish","discard","to_pre","promote","reset","diff","releases","rollback"],"type":"string"},"appId" ⟨11 unchanged words⟩ "publish\", \"promote\" and \"rollback\": accept a rooms change that discards data saved in pro","type":"boolean"},"releaseId":{ ⟨17 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schemaproperties":{"http://json-schema.org/draft-07/schema#appId",:{"additionalPropertiesdescription":false"The app public ID","propertiestype":{"appIdstring"},"avatar":{"description":"Theapp\"compatiblepublicwithIDMundo\" seal (mundo chapter)","type":"stringboolean"},"bestOn":{"description":"Device the app is best suitedfor. Omitforboth mobile and desktop.","enum":["mobile","desktop"],"type":"string"},"defaultLang":{"default":"en","enum":["en","es","fr", ⟨7 unchanged words⟩ ,"type":"string"},"descriptions":{"additionalProperties":{"type":"string"},"description":"Description per language code (max 500 chars each). At least one entry required.","type":"object"},"portraitkind":{"description":"CoverRequired","type":"string"},"modes":{"description":"Howimage.itAcceptsisanplayedhttp(s)orURLused,aseveraldataallowed","items":URI{"type":"string"},"type":"array"},"playerMoves":{"description":"Mundo:ortruerawtobase64let(JPEG/PNG/GIF/BMP/TIFF).aAuto-scaledplayertocarry400x400intoJPEG,thisstoredgameastheportrait.jpgmoves (dances","type":"boolean"},"playerMovesOff":{"description":"Productandidsset(sp_…)asofcover_image.moves this game does not allow","maxLengthitems":262144,{"type":"string"},"sectiontype":"array"},"portrait":{"description":"GalleryCoversectionimage","type":\"multiplayer\string"(real-time/social)},\"singleplayer\sandbox"(one person:{"description":"Mundo'satsandboxa(mundotime)chapter,or\"app\Powers\"(utilities/tools/sites)","enumtype":["singleplayerboolean"},"multiplayersection",:{"appdescription"]:"Retired: refused, with an error that names kind","type":"string"},"tags":{"description" ⟨6 unchanged words⟩ :"string"},"type":"array"},"titlesthemes":{"additionalPropertiesdescription":"Required","items":{"type":"string"},"descriptiontype":"Title per language codearray"},e.g. { en"titles":\{"My App\description", es:\"MiByApp\"language}.code;Atlistedleastonlyonetoentryvisitorsrequired.of those languages","type":"object"}},"required":["appId","section","titles","descriptions"],"type":"object"}
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"category":{"maxLength":40,"type":"string"},"contentType":{"description":"One of the app's community.contentTypes(any when the list is empty)","maxLength":64,"type":"string"},"i18n":{"additionalProperties":{"additionalProperties":false,"properties":{"summary":{"maxLength":500,"type":"string"},"title":{"maxLength":120,"type":"string"}},"type":"object"},"description":"Title/summary in other languages: { \ ⟨5 unchanged words⟩ , \"summary\": \"…\" } }(up to 16). Search covers every language; readers asking for a locale get it","type":"object"},"lang":{"description":"Language of title/summary, e.g. \"es\"","maxLength":8,"type":"string"},"parentId":{"description" ⟨17 unchanged words⟩ stores for this entry"},"summary":{"maxLength":500,"type":"string"},"tags":{"items":{"maxLength":32,"type":"string"},"maxItems":20,"type":"array"},"title":{"maxLength":120,"type":"string"}},"required":[" ⟨5 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": set {packageName, serviceAccountJson, track?, certs?}","enum":["set","build","status","remove"],"type":"string"},"appId" ⟨28 unchanged words⟩ :{"description":"The Play service account key, as the JSON text Google gives","type":"string"},"store":{ ⟨19 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨18 unchanged words⟩ "string"},"text":{"anyOf":[{"maxLength":1000,"type":"string"},{"type":"null ⟨14 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨30 unchanged words⟩ :"Reply text; empty string deletes the reply","maxLength":2000,"type":"string"}},"required":[ ⟨5 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"palette":{"additionalProperties":false,"description":"Hex colours, e.g. { background: \"#14131C\", primary: \"#6C8CFF\", dark: true }","properties":{"alert":{"type":"string"},"background":{"type":"string"},"dark":{"description":"true for a dark background: text and cards switch to light neutrals","type":"boolean"},"primary":{"type":"string"},"secondary":{"type":"string"}},"type":"object"},"typography":{" ⟨12 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"type":"string"},"inputs":{"additionalProperties":{},"type":"object"},"name":{"type ⟨6 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["listBy action: refund {saleId}","documentsenum",:["refund"],"type":"string"},"appId ⟨5 unchanged words⟩ ,"type":"string"},"env":{"default":"pro","description":"\"pro\" (the app's users, ⟨8 unchanged words⟩ ,"pre"],"type":"string"},"from":{"description":"ISO date","type":"string"},"limit":{"maximum":200,"minimum":1,"type":"integer"},"productId":{"type":"string"},"saleId":{"description":"The sale's id from \"list\" (documents, refund)","type":"string"},"seller":{"description":"list: one seller's sales (\"site:<id>\" or \"user:<id>\")","type":"string"},"skip":{"minimum":0,"type":"integer"},"status":{"enum":["completed","refunded","charged_back","ended"],"type":"string"},"to":{"description":"ISO date","type":"string"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": create {name, email?, photo?, language?}; update {user, name?, email?, photo?, language?}; remove {user}; book {user, set, slot, for?, party?}; buy {user, productId, amount?, at?, until?, series?}; message {user, text}; chat {user, set, slot, text}; score {user, board, value, seconds?, day?, data?}; save {user, data}; report…","enum":["list","create","update","remove","removeAll"," ⟨22 unchanged words⟩ bought it (ISO date, within the lastyear; now by default)year","type":"string"},"board":{ ⟨14 unchanged words⟩ sends to your check; save: their whole document(an object)"},"day":{"description":"score: ⟨22 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target,\"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type" ⟨12 unchanged words⟩ name, address, timeZone, phone?, about?, photo?, fields? })","properties":{},"type":"object"},"language":{"type" ⟨49 unchanged words⟩ repetition of a standing place that offers several(C285)","type":"string"},"set":{ ⟨69 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["top","player","remove","setRating"],"type":"string"} ⟨22 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"], ⟨37 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨5 unchanged words⟩ },"emailPart":{"description":"At least23 characters of the email to match","minLength":2,"type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":50,"maximum":50,"minimum":1,"type":"integer"},"skip":{"default":0,"minimum":0,"type":"integer"}},"required":[" ⟨4 unchanged words⟩
before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨34 unchanged words⟩ "message":{"description":"Plain-text message body","minLength":1,"type":"string"},"thread":{" ⟨27 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["create","rotate","status","revoke"],"type":"string"}, ⟨13 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨20 unchanged words⟩ :"Let users' clients call it (default false: only the server file)","type":"boolean"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers sent on every call, e.g. { \"Authorization\": \"Bearer {secret}\" }","type":"object"},"limits":{"additionalProperties":false,"description":"Call limits (absent or 0 = none)","properties":{"app":{"additionalProperties":false,"properties":{"day":{"$ref":"#/properties/limits/properties/perUser/properties/minute"},"month":{"$ref":"#/properties/limits/properties/perUser/properties/minute"}},"type":"object"},"loggedInOnly":{"type":"boolean"},"perUser":{"additionalProperties":false,"properties":{"day":{"$ref":"#/properties/limits/properties/perUser/properties/minute"},"hour":{"$ref":"#/properties/limits/properties/perUser/properties/minute"},"minute":{"minimum":0,"type":"integer"},"month":{"$ref":"#/properties/limits/properties/perUser/properties/minute"},"sixHours":{"$ref":"#/properties/limits/properties/perUser/properties/minute"},"week":{"$ref":"#/properties/limits/properties/perUser/properties/minute"}},"type":"object"}},"type":"object"},"name":{"description":"Secret name: a letter, then letters, digits or _ (≤ 40), e.g. CLOUDINARY","type":"string"},"query":{"additionalProperties":{"type":"string"},"description":"Query parameters sent on every call, e.g. { \"key\": \"{secret}\" }","type":"object"},"value":{"description":"The secret itself (≤ 8 KB). Required on create; leave out on a change to keep it","type":"string"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"additionalProperties":false,"description":"Whatkind:itopenRoomdoes|(ormessageuse|template)","properties":{"body":{},"config":{},"data":{},"kind":{"enum":["openRoom","message","callApi"],"type":"string"},"method":{"type":"string"},"openForMinutes":{"type":"integer"},"path":{"type":"string"},"room":{"type":"string"},"secret":{"type":"string"}},"required":["kind"],"type":"object"},"appId":{"description":"The ⟨66 unchanged words⟩ a letter, then letters, digits, _ or- (≤ 40)","type":"string"},"path":{ ⟨6 unchanged words⟩ live room whose server file hears it asa hook","type":"string"},"secret":{ ⟨32 unchanged words⟩ {"type":"integer"},"when":{"additionalProperties":false,"description":"When it runs","properties":{"at":{"type":"string"ISO},"daily":| {"type"every:"string"minutes 5-10080},"days":| {"items"daily:{"type\"HH:"integerMM\"},"type":"array"}days?,"every":{"type":"integer"tz?},"tz":{"type":"string"}},"type":"object"}},"required":[ ⟨5 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨4 unchanged words⟩ :"string"},"status":{"description":"Newterminatedbansealsstatethe device and \"none\" cannot undo it; banned is reversible","enum":["none","banned"," ⟨19 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨11 unchanged words⟩ (the users', default) or \"pre\"(its own unless it plays with pro's data)","enum":["pro","pre"], ⟨25 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"type":"string" ⟨27 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"checkOnly":{"description":"Only check availability, change nothing","type":"boolean"},"move":{"description":"Take it from another app of yours that holds it(its slug or an old one)","type":"boolean"},"slug":{ ⟨19 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
—after
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enum":["status","set_head_office","set_defaults","add_registration","expire_registration","report","app"],"type":"string"},"address":{"additionalProperties":false,"properties":{"city":{"type":"string"},"country":{"type":"string"},"line1":{"type":"string"},"line2":{"type":"string"},"postal_code":{"type":"string"},"state":{"type":"string"}},"required":["country"],"type":"object"},"appId":{"type":"string" ⟨19 unchanged words⟩ app: the tax is included in the price(default true) or added on top (false)","type":"boolean"},"projectId":{"description":"A project with its own Stripe(C220): the taxes of its payout account.","type":"string"},"registrationId":{ ⟨37 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"access":{"description":"One ⟨11 unchanged words⟩ ,"type":"string"},"action":{"enumdescription":["By action: write {nothing when absent}; partner {everything but money, the plan and deleting the app}; support {the people of the app, the Business tab, and the sales, read only}; viewer {everything, read only}; staff {Business, only what is theirs}; add"{email | friend, groups?: [ids], access?, panels?: [names] | \"resend*\","removerole?…","listenum",:["lookupadd","friendsresend","groupsremove","group_save","group_delete"],"type":" ⟨13 unchanged words⟩ be accepted in (7 by default)","maximum":30,"minimum":1,"type":"integer"},"friend":{"description" ⟨20 unchanged words⟩ of the app's groups or ready-made ones (partner, support, viewer, screens, staff)","items":{"type":"string"}, ⟨12 unchanged words⟩ :"group_save: exactly who is in it (emailsreplacesorthekeyslist)","items":{"type":"string"} ⟨23 unchanged words⟩ :"boolean"},"owner":{"description":"add:trueinvites them to becomehands theapp's owner;wholefalseappwithdrawsoverthatwhenofferaccepted","type":"boolean"},"panels":{ ⟨8 unchanged words⟩ type":"string"}]},"rights":{"additionalProperties":{"enum":["read","write"],"type":"string"},"description":"group_save: { area: \"read\" ⟨20 unchanged words⟩ sites of the business its Business is limitedto (C350);to; [] or null: all of it"}},"required":["appId","action" ⟨2 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":properties"http://json-schema.org/draft-07/schema#",{"additionalPropertiesaction":false,{"propertiesdescription":{"By action": cancel {id}","enum":["list","cancel"],"type":"string"},"appId" ⟨13 unchanged words⟩ test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"], ⟨38 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}before
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series: [seriesIds] } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }; several ids = one product with several timetables to choose from, the client picks one when buying (shop.buy(productId, 1, { series })) and changes it later with no payment (booking.changeStanding); one id alone works as one). A service with moveHours (hours) lets its clients move a booking to another session themselves (booking.move) up to that many hours before. early { products: [productId…], hours: 12 } (C382): a new session (opened by the business, or an open match a player makes) is seen and booked during its first hours only by whoever has one of those products (bought, or a live subscription) and whoever made it; slots gives them earlyUntil, booking earlier answers early_access with until. Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"draws":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"cards":{"items":{"additionalProperties":false,"properties":{"copies":{"type":"integer"},"data":{"additionalProperties":{},"type":"object"}},"required":["data"],"type":"object"},"type":"array"},"cost":{"anyOf":[{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},"type":"array"}]},"free":{"anyOf":[{"additionalProperties":false,"properties":{"everyHours":{"type":"number"},"max":{"type":"integer"}},"required":["everyHours"],"type":"object"},{"type":"null"}]},"into":{"type":"string"},"perOpen":{"type":"integer"}},"required":["into","cards"],"type":"object"},"type":"object"},{"type":"null"}],"description":"Fair draws (C311): a pack of cards, a loot box, a daily prize, a discount to win. Opened by the user with neblla.db.draw(name) (neblla.db.drawInfo(name): the chance of each card, the cost, the free ones, the last opening). Each opening, Neblla shuffles the whole deck anew, deals perOpen cards and writes each one as a row of the user in `into` BEFORE answering; neither the client nor the Fog decides cards or how many. { <name>: { into: a table with write: \"neblla\", cards: [{ copies (1 by default; the rarity: 90 common, 9 rare, 1 legendary), data: the row (fits the table's fields) }] (up to 500, 100000 copies in all), perOpen (1–50, 1 by default), cost?: [{ credits: a credit kind of business.credits (a pack bought in the shop, the welcome gift, coins), n }] (the user picks with { pay: kind }; by default a free one, else the first they can pay), free?: { everyHours, max? (1) } (a free opening every so many hours, up to max kept) } }; at least a cost or free. The log: neblla_draws. Replaces the whole object; null clears them"},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matches":{"anyOf":[{"anyOf":[{"const":true,"type":"boolean"},{"const":false,"type":"boolean"},{"additionalProperties":true,"properties":{"board":{"type":"string"},"cheater":{"enum":["block","ban"],"type":"string"},"cheaterHint":{"additionalProperties":false,"properties":{"lost":{"type":"integer"},"ratio":{"type":"number"}},"type":"object"},"reportHours":{"type":"number"}},"type":"object"}]},{"type":"null"}],"description":"The games of the app's rooms as matches (C372: online chess, a duel, a 2v2), with the same results, ELO and disputes as a service with `results` (neblla_matches). Each game started in a room (neblla.room.start()) is a match between side A and side B of its signed-in players (by order of arrival, or neblla.room.start({ teams: { A: [ids], B: [ids] } })); each player says who won with neblla.room.matches.report(neblla.room.match, { winner: \"A\" | \"B\" | \"draw\", score? }). { reportHours (48), board? (an ELO board of scores the results move), cheaterHint?, cheater? (\"block\" | \"ban\") }; true: the defaults; null or false: off"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"k":{"description":"elo: how much one match moves it (32 by default, up to 200)","type":"number"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"},"start":{"description":"elo: everyone's first rating (1200 by default)","type":"integer"},"type":{"description":"\"elo\" (C353): a level between players moved by the results of matches (neblla_matches), not by start/submit; with start and k only","enum":["elo"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }, or an ELO level { board: { type: \"elo\", start?: 1200, k?: 32 } } that the results of the matches of a service move (business.booking.<service>.results.board); names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"},"write":{"enum":["player","neblla"],"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never), write?: \"neblla\" (only Neblla writes its rows: a draw, a trade, the admin API, this MCP, the panel and a real server; the user reads them and db.save / db.delete answer table_read_only: a collection of cards, a balance of coins, marks) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking","business_application","match_disputed"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}after
{"properties":{"addDomain":{"description":"Append a domain entry","type":"string"},"appId":{"description":"The app public ID","type":"string"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron)","type":"boolean"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\")","type":["string","null"]},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true)","type":"boolean"},"example":{"description":"true publishes the code to a public GitHub repo, slug becomes -example, test-mode sales","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message","type":"object"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours","type":["boolean","null"]},"keys":{"description":"Public values the app reads with neblla.api.key(name): text, number","type":"object"},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"pattern":{"description":"App pattern id","type":"string"},"playerQuota":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in %"},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player","type":["boolean","null"]},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"theme":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey }"}},"required":["appId"],"type":"object"}before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"category":{"maxLength":40,"type":"string"},"entryId":{"description" ⟨4 unchanged words⟩ string"},"i18n":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"summary":{"maxLength":500,"type":"string"},"title":{"maxLength":120,"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Title/summary in other languages; replaces the whole map, null clears it"},"lang":{"maxLength":8,"type":"string"},"payload":{"description":"Replaces the stored payload whole"},"summary":{"maxLength":500,"type":"string"},"tags":{"items":{"maxLength":32,"type":"string"},"maxItems":20,"type":"array"},"title":{"maxLength":120,"type":"string"}},"required":[" ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨10 unchanged words⟩ room data (replaces the whole object)","properties":{},"type":"object"},"moderators":{"description" ⟨20 unchanged words⟩ "Days without activity before Neblla deletes this room(0 = never). Overrides the app's per-type value (rooms.public/private.ttlInactivity: 30 / 90 by default) for this room only.","maximum":3650,"minimum":0,"type":"integer"},{"type":"null"}],"description":"Days without activity before Neblla deletes this room(0 = never). Overrides the app's per-type value (rooms.public/private.ttlInactivity: 30 / 90 by default) for this room only."}},"required":["appId","name" ⟨2 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The app public ID","type":"string"},"data":{"additionalProperties":{},"description":"The replacement data object (must be a plain object, not array/null)","type":"object"},"userId":{ ⟨14 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"The ⟨4 unchanged words⟩ string"},"content":{"description":"Filecontent. For text files: the raw source as-is (NOT base64). For binary files: a base64 string. Required UNLESS `contentUrl` is set.content","type":"string"},"contentUrl":{"description":"Public URL of the file toupload. Neblla downloads it server-side (no need to base64-encode in this conversation). Use this for any binary asset (images, fonts, audio, video) when you have a URL. Mutually exclusive with `content`.upload","type":"string"},"encoding":{"default":"utf8","description":"Content encoding for`content`. Ignored when `contentUrl` is used. Use \"utf8\" for all text files (HTML/CSS/JS/TS/JSON/SVG); \"base64\" only for binary assets.`content`","enum":["utf8","base64"], ⟨3 unchanged words⟩ {"description":"A short note about thischange. It names this version in the app history (neblla_versions), so the history reads like a changelog instead of \"upload: 3 file(s)\". A burst of uploads is one version: pass the same note on each.","maxLengthchange":120,"type":"string"},"path":{"description":"File path within theapp. Canonical: \"index.html\", \"main.css\", \"client.js\", \"server/room.js\". Subfolders allowed for assets.app","type":"string"}},"required": ⟨4 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"decisions":{"additionalProperties":{},"default":{},"description":"Decisions answered so far, keyedKeyed bydecision id. Exampleid:{kind: \"game\", players: \"multi\", modules: [\"social\",\"anticheat\"]connections, palette: { primary: \"#0F7BD7\",secondary: \"#15C2B0\", alert: \"#FF8552\" }, typography: \"techy\" }. Include everything the user stated plus everything you inferred.typography…","type":"object"}},"type":"object"}
before
—after
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"action":{"enumdescription":["historyBy action: pre {every save burst is a commit}; restore {commit, branch?}; migrate {url, toBranch?, toProBranch?}","filesenum",:["restore","migrate"],"type":"string" ⟨11 unchanged words⟩ history / files / restore: which environment's branch(default \"pre\")","enum":["pre","pro"], ⟨74 unchanged words⟩
before
—after
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false}⟨30 unchanged words⟩ ,"rateUser","userRating","reviews","hideReview","sellers","addSeller","removeSeller","sellerStatus","sellerLink"],"type":"string"},"address":{ ⟨177 unchanged words⟩ ":{"description":"hideReview: the review's id","type":"string"},"seller":{"description":"addSeller, removeSeller, sellerStatus, sellerLink: \"site:<site id>\" or \"user:<user id>\"","type":"string"},"site":{ ⟨61 unchanged words⟩
⟨242 unchanged words⟩ user content, a shop where the developer sells,—anallappfit.ofAmanytruesellersmarketplaceeachwherepaidusersonselltheirtoownEACHStripeOTHER—doesallnot:fit.payoutsUsersgoreselling totheEACHdeveloperOTHERonly.what they bought does not. Only if the page runs entirely in one ⟨743 unchanged words⟩
⟨191 unchanged words⟩ home), with no feature pieces and notheme.theme, and the modules of that page. Prefer `decisions`. Omit both for a blankapp.app: only auth and db, the rest with neblla_update_app({ features }).","type":"string"}},"required": ⟨3 unchanged words⟩
⟨211 unchanged words⟩ : say so when a shop needs them.PayoutsWHOgoIStoPAID: by default the app's payout account (the developer,sooronethesellerproject'stoownmanyStripe).buyersAnworksappandwith MANY SELLERS (the clubs of amulti-sellerpadelmarketplaceapp,doesthenot.artists of a gallery, the authors of a shop of templates, the teachers of a platform) gives a product a `seller` ("site:<id>", a site of the business; "user:<id>", a user of the app): it is charged on THAT seller's own Stripe account, which pays its refunds and disputes, and the developer takes their commission (neblla_update_app business.sellers.fee) — see neblla_business "sellers". Until the seller's account is active the product is listed as waiting and its checkout answers seller_waiting. Reselling between users is not there. WHAT IS SOLD IS OFTEN NOT A FILE ⟨122 unchanged words⟩
⟨356 unchanged words⟩ onboarding).","type":"boolean"},"seller":{"description":"Who is paid for it (C400): \"site:<id>\" or \"user:<id>\", one of the app's sellers (made one if new); omit for the app itself","type":"string"},"shipping":{"additionalProperties":false,"description":"Physical ⟨162 unchanged words⟩
⟨280 unchanged words⟩ make one commit each). "unlink". "example" — an app marked as an example (neblla_update_app example): its pro version goes to the developer's public repo of examples (<login>/neblla-examples, one folder per app) now, written again even if it is there; it also goes by itself at each neblla_promote, when it is marked and when the developer links GitHub. For trying again after an error (neblla_get_app shows examplePublish). A sync downloads only the files that changed, ⟨27 unchanged words⟩
⟨12 unchanged words⟩ ,"sync","commit","set_mode","unlink","example"],"type":"string"},"appId":{ ⟨197 unchanged words⟩
⟨11 unchanged words⟩ " { status?, productId?, from?, to?, seller?, skip?, limit? } — newest first, each ⟨21 unchanged words⟩ , the buyer's userId, email and buyerName,canRefund.canRefund, and the seller who was paid (seller, sellerName: one of the app's sellers, neblla_business "sellers"; none: the app). "documents" { saleId } — the links Stripe ⟨25 unchanged words⟩ back in Stripe (Neblla's 1 % with it; a seller's sale is given back by the seller, with the developer's commission when it can be recovered) and takes the purchase back at once ⟨67 unchanged words⟩
⟨66 unchanged words⟩ , refund)","type":"string"},"seller":{"description":"list: one seller's sales (\"site:<id>\" or \"user:<id>\")","type":"string"},"skip":{"minimum":0,"type":"integer ⟨19 unchanged words⟩
⟨329 unchanged words⟩ in neblla_create_product)","type":"boolean"},"seller":{"description":"Who is paid for it (C400): \"site:<id>\" or \"user:<id>\"; null: the app again. See neblla_create_product","type":["string","null"]},"shipping":{"anyOf":[{"additionalProperties":false, ⟨175 unchanged words⟩
⟨151 unchanged words⟩ app); "update" { user, name?, email? (…@ejemplo.neblla.app, e.g. one that goes with a new name; their place in the team and as a professional follows), photo? (null takes it off), language? } ⟨387 unchanged words⟩
⟨26 unchanged words⟩ \"pre\" (its test copy: pre'sservices andconfig, on pre's data, its own unless ⟨60 unchanged words⟩
⟨15 unchanged words⟩ ,"draw":{"type":"string"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"limit":{"type":"integer"},"user" ⟨7 unchanged words⟩
⟨16 unchanged words⟩ ,"appId":{"type":"string"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"group":{"type":"string"},"name" ⟨13 unchanged words⟩
⟨31 unchanged words⟩ between side "A" and "B"; and with neblla_update_app({ matches }) (C372) each game a room starts is one too (its id room_…, its title the room's name), reported with neblla.room.matches.report; its players say who won with neblla.business.matches.report. When ⟨150 unchanged words⟩
⟨28 unchanged words⟩ \"pre\" (its test copy: pre'sservices andconfig, on pre's data, its own unless ⟨82 unchanged words⟩
⟨95 unchanged words⟩ \"pre\" (its test copy: pre'sservices andconfig, on pre's data, its own unless ⟨42 unchanged words⟩ {"type":"string"},"form":{"$refadditionalProperties":"#/properties/data"true,"description":"apply: what neblla.business.apply takes ( ⟨3 unchanged words⟩ , phone?, about?, photo?, fields? })","properties":{},"type":"object"},"language":{"type":"string"},"match" ⟨31 unchanged words⟩ {"type":"string"},"seconds":{"$ref":"#/properties/value","description":"score: how long the game took","type":"number"},"series":{"description":"buy: ⟨82 unchanged words⟩
⟨22 unchanged words⟩ ,"board":{"type":"string"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"limit":{"type":"integer"},"period" ⟨31 unchanged words⟩
⟨19 unchanged words⟩ rows match","type":"boolean"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"limit":{"description":"Up to 1000 ( ⟨61 unchanged words⟩
⟨16 unchanged words⟩ additionalProperties":{},"type":"object"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"group":{"description":"Or a group ( ⟨35 unchanged words⟩
⟨12 unchanged words⟩ ,"appId":{"type":"string"},"env":{"description":"\"pro\" (the published app) or \"pre\" (its test copy: pre's config, on pre's data, its own unless it plays with pro's). Left out: where the app is, its chosen environment (neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"id":{"description":"The trade (cancel) ⟨31 unchanged words⟩
before
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series: [seriesIds] } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }; several ids = one product with several timetables to choose from, the client picks one when buying (shop.buy(productId, 1, { series })) and changes it later with no payment (booking.changeStanding); one id alone works as one). A service with moveHours (hours) lets its clients move a booking to another session themselves (booking.move) up to that many hours before. Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"draws":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"cards":{"items":{"additionalProperties":false,"properties":{"copies":{"type":"integer"},"data":{"additionalProperties":{},"type":"object"}},"required":["data"],"type":"object"},"type":"array"},"cost":{"anyOf":[{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},"type":"array"}]},"free":{"anyOf":[{"additionalProperties":false,"properties":{"everyHours":{"type":"number"},"max":{"type":"integer"}},"required":["everyHours"],"type":"object"},{"type":"null"}]},"into":{"type":"string"},"perOpen":{"type":"integer"}},"required":["into","cards"],"type":"object"},"type":"object"},{"type":"null"}],"description":"Fair draws (C311): a pack of cards, a loot box, a daily prize, a discount to win. Opened by the user with neblla.db.draw(name) (neblla.db.drawInfo(name): the chance of each card, the cost, the free ones, the last opening). Each opening, Neblla shuffles the whole deck anew, deals perOpen cards and writes each one as a row of the user in `into` BEFORE answering; neither the client nor the Fog decides cards or how many. { <name>: { into: a table with write: \"neblla\", cards: [{ copies (1 by default; the rarity: 90 common, 9 rare, 1 legendary), data: the row (fits the table's fields) }] (up to 500, 100000 copies in all), perOpen (1–50, 1 by default), cost?: [{ credits: a credit kind of business.credits (a pack bought in the shop, the welcome gift, coins), n }] (the user picks with { pay: kind }; by default a free one, else the first they can pay), free?: { everyHours, max? (1) } (a free opening every so many hours, up to max kept) } }; at least a cost or free. The log: neblla_draws. Replaces the whole object; null clears them"},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"},"write":{"enum":["player","neblla"],"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never), write?: \"neblla\" (only Neblla writes its rows: a draw, a trade, the admin API, this MCP, the panel and a real server; the user reads them and db.save / db.delete answer table_read_only: a collection of cards, a balance of coins, marks) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking","business_application","match_disputed"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}after
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series: [seriesIds] } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }; several ids = one product with several timetables to choose from, the client picks one when buying (shop.buy(productId, 1, { series })) and changes it later with no payment (booking.changeStanding); one id alone works as one). A service with moveHours (hours) lets its clients move a booking to another session themselves (booking.move) up to that many hours before. early { products: [productId…], hours: 12 } (C382): a new session (opened by the business, or an open match a player makes) is seen and booked during its first hours only by whoever has one of those products (bought, or a live subscription) and whoever made it; slots gives them earlyUntil, booking earlier answers early_access with until. Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"draws":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"cards":{"items":{"additionalProperties":false,"properties":{"copies":{"type":"integer"},"data":{"additionalProperties":{},"type":"object"}},"required":["data"],"type":"object"},"type":"array"},"cost":{"anyOf":[{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},"type":"array"}]},"free":{"anyOf":[{"additionalProperties":false,"properties":{"everyHours":{"type":"number"},"max":{"type":"integer"}},"required":["everyHours"],"type":"object"},{"type":"null"}]},"into":{"type":"string"},"perOpen":{"type":"integer"}},"required":["into","cards"],"type":"object"},"type":"object"},{"type":"null"}],"description":"Fair draws (C311): a pack of cards, a loot box, a daily prize, a discount to win. Opened by the user with neblla.db.draw(name) (neblla.db.drawInfo(name): the chance of each card, the cost, the free ones, the last opening). Each opening, Neblla shuffles the whole deck anew, deals perOpen cards and writes each one as a row of the user in `into` BEFORE answering; neither the client nor the Fog decides cards or how many. { <name>: { into: a table with write: \"neblla\", cards: [{ copies (1 by default; the rarity: 90 common, 9 rare, 1 legendary), data: the row (fits the table's fields) }] (up to 500, 100000 copies in all), perOpen (1–50, 1 by default), cost?: [{ credits: a credit kind of business.credits (a pack bought in the shop, the welcome gift, coins), n }] (the user picks with { pay: kind }; by default a free one, else the first they can pay), free?: { everyHours, max? (1) } (a free opening every so many hours, up to max kept) } }; at least a cost or free. The log: neblla_draws. Replaces the whole object; null clears them"},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matches":{"anyOf":[{"anyOf":[{"const":true,"type":"boolean"},{"const":false,"type":"boolean"},{"additionalProperties":true,"properties":{"board":{"type":"string"},"cheater":{"enum":["block","ban"],"type":"string"},"cheaterHint":{"additionalProperties":false,"properties":{"lost":{"type":"integer"},"ratio":{"type":"number"}},"type":"object"},"reportHours":{"type":"number"}},"type":"object"}]},{"type":"null"}],"description":"The games of the app's rooms as matches (C372: online chess, a duel, a 2v2), with the same results, ELO and disputes as a service with `results` (neblla_matches). Each game started in a room (neblla.room.start()) is a match between side A and side B of its signed-in players (by order of arrival, or neblla.room.start({ teams: { A: [ids], B: [ids] } })); each player says who won with neblla.room.matches.report(neblla.room.match, { winner: \"A\" | \"B\" | \"draw\", score? }). { reportHours (48), board? (an ELO board of scores the results move), cheaterHint?, cheater? (\"block\" | \"ban\") }; true: the defaults; null or false: off"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"k":{"description":"elo: how much one match moves it (32 by default, up to 200)","type":"number"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"},"start":{"description":"elo: everyone's first rating (1200 by default)","type":"integer"},"type":{"description":"\"elo\" (C353): a level between players moved by the results of matches (neblla_matches), not by start/submit; with start and k only","enum":["elo"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }, or an ELO level { board: { type: \"elo\", start?: 1200, k?: 32 } } that the results of the matches of a service move (business.booking.<service>.results.board); names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"},"write":{"enum":["player","neblla"],"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never), write?: \"neblla\" (only Neblla writes its rows: a draw, a trade, the admin API, this MCP, the panel and a real server; the user reads them and db.save / db.delete answer table_read_only: a collection of cards, a balance of coins, marks) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking","business_application","match_disputed"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}⟨418 unchanged words⟩ text }, "removeUserNote" { id }. Businesses that apply from the app (C351: clubs in a padel app, coworkings in a network), with neblla_update_app({ business: { apply: { fields?: [{ key, label, type ("text", "number", "boolean"), required? }], group? (the id of a group of neblla_team whose rights the new site's group copies; none: Business to write) } } }) and neblla.business.apply() in the app: "applications" { status? ("pending" by default, "approved", "rejected", "all"; the oldest first, with the applicant's name and email) }, "approve" { application } (makes the site with what they wrote, a group of the team limited to it with the template's rights, and invites the applicant to the team with their Neblla account; they hear it in on('update')), "reject" { application, reason? } (nothing is deleted; they read the reason and can apply again). Sites also take phone?, about?, photo? (https) and fields? (the answers to the app's own questions); neblla.business.booking.services() gives them to the app, with rating and ratingCount. Ratings both ways after a session already played (C356): clients rate the site with neblla.business.rate(); "rateUser" { user, slot, rating (1-5), text? } is the business rating a client of that session (once per session; again changes it), "userRating" { user } their average and what the businesses said (only the businesses see it; the agenda brings clientRating on each booking), "reviews" { kind? ("sites", the default, or "users"), id? (a site, or a user), hidden? } to moderate them, newest first, and "hideReview" { review, hidden? } hides one (it stays, counts for nothing and nobody reads it; hidden: false shows it again). Sessions are made with neblla_booking createSlot (site, ⟨18 unchanged words⟩
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"about":{"type":["string","null"]},"action":{"enum":["list","save" ⟨9 unchanged words⟩ ,"removeUserNote","close","closures","reopen","applications","approve","reject","rateUser","userRating","reviews","hideReview"],"type":"string"},"address":{ ⟨6 unchanged words⟩ ,"appId":{"type":"string"},"application":{"description":"approve, reject: the application's id","type":"string"},"capacity":{"type":"integer"},"closure" ⟨78 unchanged words⟩ ,"pre"],"type":"string"},"fields":{"anyOf":[{"additionalProperties":{},"type":"object"},{"type":"null"}]},"from":{"type":"string"},"hidden":{"description":"reviews: only the hidden (true) or the shown (false); hideReview: false shows it again","type":"boolean"},"hours":{},"id":{"type":" ⟨2 unchanged words⟩ "enum":["sites","resources","staff","users"],"type":"string"},"limit":{"type":"integer"},"name":{"type":"string"},"note":{"type":"string"},"phone":{"type":["string","null"]},"photo":{"type":["string","null"]},"rating":{"description":"rateUser: 1 to 5","type":"integer"},"reason":{"description":"close: what clients read («Festivo»)","type":"string"},"review":{"description":"hideReview: the review's id","type":"string"},"site":{"type":["string","null" ⟨6 unchanged words⟩ :"array"},{"type":"null"}]},"slot":{"description":"rateUser: the session they were in","type":"string"},"status":{"enum":["pending","approved","rejected","all"],"type":"string"},"text":{"type":"string"}, ⟨28 unchanged words⟩
⟨228 unchanged words⟩ buyers works and a multi-seller marketplace does not. WHAT IS SOLD IS OFTEN NOT A FILE: `content` gives a one_time product or a subscription private links or texts (a Canva template «Abrir en Canva», a hidden course video, a game code, a Discord invite), read only by who holds it with neblla.media.mine(); files are unlocked with neblla_media "link". UNITS FOR SALE (stock, one_time and consumable) ⟨71 unchanged words⟩
⟨10 unchanged words⟩ public ID","type":"string"},"content":{"description":"PRIVATE CONTENT the buyer gets beside its files (one_time: for good; subscription: while it lives): a link (a Canva template, a hidden video, a Discord invite) or a short text (a code, instructions). Never in the public catalogue; the buyer reads it with neblla.media.mine() (`content`). Neblla controls who sees it, not whether the buyer passes it on","items":{"anyOf":[{"additionalProperties":false,"properties":{"label":{"anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"What the button says, e.g. \"Abrir en Canva\" or { es, en }"},"type":{"const":"link","type":"string"},"url":{"description":"http(s) address","type":"string"}},"required":["type","label","url"],"type":"object"},{"additionalProperties":false,"properties":{"label":{"anyOf":[{"$ref":"#/properties/content/items/anyOf/0/properties/label/anyOf/0"},{"$ref":"#/properties/content/items/anyOf/0/properties/label/anyOf/1"}]},"text":{"description":"Up to 2000 characters: a code, access instructions","type":"string"},"type":{"const":"text","type":"string"}},"required":["type","text"],"type":"object"}]},"maxItems":10,"type":"array"},"currency":{"description":"ISO 4217 code Stripe ⟨366 unchanged words⟩
⟨97 unchanged words⟩ @ejemplo.neblla.app. They count in no quota, nostatistic, no«write to everyone» and noexport.export; their visits in no statistic (their sales of example do, marked). action "list"; "create" { name, ⟨40 unchanged words⟩ with everything of theirs: bookings, notes, messages, sales of example). What they do, so the back ⟨26 unchanged words⟩ "buy" { user, productId, amount?, at?, until?, series? } (any product: a SALE OF EXAMPLE, marked so, in Ventas, on their card and in the statistics, with no Stripe and no money, out of quotas, the sales CSV and Stripe Tax; it gives what the product gives — files and private content, credits of a pack, a standing placein— and fires the purchase webhook with sample: true. at: when, within the last year, to spread the sales over the last days; until: arepetitionsubscription—lives until then and ends (without it, it renews by itself). Refund one with neblla_sales "refund", as a real one: nopaymentStripe, it takes back what it gave andnothingfiresintheVentasrefund webhook), "message" { user, text } ⟨15 unchanged words⟩ in the chat of a session they booked), "score" { user, board, value, seconds? (how long the game took, 60 by default), day? (YYYY-MM-DD, today or up to 30 days back; play the oldest first), data? (for your own check) } (a game of a ranking of neblla.scores, through the SAME checks as a real one: a game a day, the limits, your check; one that does not pass is refused, logged in «Errores y rechazos» and marks them like a real cheat, so the anti-cheats can be shown) → what neblla.scores.submit answers, "save" { user, data } (their neblla.db document, as if they had saved it: replaces it whole). "book" also takes team ("A" or "B", a match) and a session already played (the past of an example); "report" { user, match, winner, score? } (they say a match's result, as neblla.business.matches.report: two that disagree make a dispute), "rate" { user, slot, rating, text?, site? } (they rate the site of a session they were in, as neblla.business.rate), "apply" { user, form } (a business applies to be in the app, as neblla.business.apply). Notes on their card: neblla_business userNote ⟨21 unchanged words⟩
⟨13 unchanged words⟩ ,"book","buy","message","chat","score","save","report","rate","apply"],"type":"string"},"amount":{"type":"integer"},"appId":{"type":"string"},"at":{"description":"buy: when they bought it (ISO date, within the last year; now by default)","type":"string"},"board":{"description":"score: the ranking (neblla_update_app scores)","type":"string"},"data":{"description":"score: what the game sends to your check; save: their whole document (an object)"},"day":{"description":"score: YYYY-MM-DD, a game of that day","type":"string"},"email":{"type":"string"},"env" ⟨63 unchanged words⟩ ,"for":{"type":"string"},"form":{"$ref":"#/properties/data","description":"apply: what neblla.business.apply takes ({ name, address, timeZone, phone?, about?, photo?, fields? })"},"language":{"type":"string"},"match":{"description":"report: the match (its session's id)","type":"string"},"name":{"type":"string"},"party" ⟨6 unchanged words⟩ ,"productId":{"type":"string"},"rating":{"description":"rate: 1 to 5","type":"integer"},"score":{"type":"string"},"seconds":{"$ref":"#/properties/value","description":"score: how long the game took"},"series":{"description":"buy: which repetition of a standing place that offers several (C285)","type":"string"},"set":{"type":"string"},"site":{"description":"rate: the site of the session","type":"string"},"slot":{"type":"string"},"team":{"description":"book: the side of a match (a service with results)","enum":["A","B"],"type":"string"},"text":{"type":"string"},"until" ⟨4 unchanged words⟩ The sample user's id (create answers it)","type":"string"},"value":{"description":"score: the score sent","type":"number"},"winner":{"enum":["A","B","draw"],"type":"string"}},"required":[ ⟨4 unchanged words⟩
⟨78 unchanged words⟩ the app log (neblla_get_logs, area score). An ELO board ({ type: "elo", start: 1200, k: 32 }, C353) moves with the results of matches (neblla_matches): "player" gives { rating, played, won, drawn, lost, place, history }; action "setRating" { board, player, rating } sets where a player starts (their level known elsewhere), and the matches after count from it.
⟨7 unchanged words⟩ "enum":["top","player","remove","setRating"],"type":"string"},"appId":{ ⟨23 unchanged words⟩ id in the app","type":"string"},"rating":{"description":"setRating: the rating they start from","type":"integer"}},"required":["appId","action" ⟨3 unchanged words⟩
⟨470 unchanged words⟩ " { id?, name, rights, own?, sites?, panels?, members?: [emails] } creates or changes one (members, when given, are exactly who is initit; sites: ids of the business's sites — neblla_business kind "sites" — its Business is limited to: its people see and change only the sessions, bookings, client cards, places, closures, chats and accounts of those sites, by every road; such a group gives no people nor moderation area, which mix every site; [] or null: the whole business); "group_delete" { id } (its ⟨268 unchanged words⟩
⟨189 unchanged words⟩ "},"role":{"type":"string"},"sites":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"description":"group_save: the sites of the business its Business is limited to (C350); [] or null: all of it"}},"required":["appId","action"],"type":"object"}
before
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"type":"string"},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }). Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}after
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series: [seriesIds] } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }; several ids = one product with several timetables to choose from, the client picks one when buying (shop.buy(productId, 1, { series })) and changes it later with no payment (booking.changeStanding); one id alone works as one). A service with moveHours (hours) lets its clients move a booking to another session themselves (booking.move) up to that many hours before. Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"draws":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"cards":{"items":{"additionalProperties":false,"properties":{"copies":{"type":"integer"},"data":{"additionalProperties":{},"type":"object"}},"required":["data"],"type":"object"},"type":"array"},"cost":{"anyOf":[{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"credits":{"type":"string"},"n":{"type":"integer"}},"required":["credits"],"type":"object"},"type":"array"}]},"free":{"anyOf":[{"additionalProperties":false,"properties":{"everyHours":{"type":"number"},"max":{"type":"integer"}},"required":["everyHours"],"type":"object"},{"type":"null"}]},"into":{"type":"string"},"perOpen":{"type":"integer"}},"required":["into","cards"],"type":"object"},"type":"object"},{"type":"null"}],"description":"Fair draws (C311): a pack of cards, a loot box, a daily prize, a discount to win. Opened by the user with neblla.db.draw(name) (neblla.db.drawInfo(name): the chance of each card, the cost, the free ones, the last opening). Each opening, Neblla shuffles the whole deck anew, deals perOpen cards and writes each one as a row of the user in `into` BEFORE answering; neither the client nor the Fog decides cards or how many. { <name>: { into: a table with write: \"neblla\", cards: [{ copies (1 by default; the rarity: 90 common, 9 rare, 1 legendary), data: the row (fits the table's fields) }] (up to 500, 100000 copies in all), perOpen (1–50, 1 by default), cost?: [{ credits: a credit kind of business.credits (a pack bought in the shop, the welcome gift, coins), n }] (the user picks with { pay: kind }; by default a free one, else the first they can pay), free?: { everyHours, max? (1) } (a free opening every so many hours, up to max kept) } }; at least a cost or free. The log: neblla_draws. Replaces the whole object; null clears them"},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"},"write":{"enum":["player","neblla"],"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never), write?: \"neblla\" (only Neblla writes its rows: a draw, a trade, the admin API, this MCP, the panel and a real server; the user reads them and db.save / db.delete answer table_read_only: a collection of cards, a balance of coins, marks) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking","business_application","match_disputed"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"content":{"anyOf":[{"description":"PRIVATE CONTENT the buyer gets beside its files (one_time: for good; subscription: while it lives): a link (a Canva template, a hidden video, a Discord invite) or a short text (a code, instructions). Never in the public catalogue; the buyer reads it with neblla.media.mine() (`content`). Neblla controls who sees it, not whether the buyer passes it on","items":{"anyOf":[{"additionalProperties":false,"properties":{"label":{"anyOf":[{"type":"string"},{"additionalProperties":{"type":"string"},"type":"object"}],"description":"What the button says, e.g. \"Abrir en Canva\" or { es, en }"},"type":{"const":"link","type":"string"},"url":{"description":"http(s) address","type":"string"}},"required":["type","label","url"],"type":"object"},{"additionalProperties":false,"properties":{"label":{"anyOf":[{"$ref":"#/properties/content/anyOf/0/items/anyOf/0/properties/label/anyOf/0"},{"$ref":"#/properties/content/anyOf/0/items/anyOf/0/properties/label/anyOf/1"}]},"text":{"description":"Up to 2000 characters: a code, access instructions","type":"string"},"type":{"const":"text","type":"string"}},"required":["type","text"],"type":"object"}]},"maxItems":10,"type":"array"},{"type":"null"}],"description":"Private content (replaces the whole list; null or [] takes it away). See neblla_create_product"},"currency":{"description":"ISO 4217 code Stripe ⟨341 unchanged words⟩
Fair draws (C311: a pack of cards, a loot box, a daily prize; declared with neblla_update_app({ draws }), opened by the user with neblla.db.draw(name)). Action "info" { draw }: the chance of each card, the cost and the free openings. Action "opens" { user? (their id in the app), draw?, limit? (up to 500, 50 by default) } → { opens }: the log of openings, newest first, each with owner, draw, at, paid ({ free } or { credits, n }) and the cards dealt (rows, with their id). To give a user packs, adjust their credits (neblla_credits).
Results of matches (C352): a service with `results` ({ teams: 2, reportHours: 48, board?, levelRange?, cheaterHint?, cheater? } in neblla_update_app({ business: { booking: { <service>: … } } })) makes each of its sessions, once played, a match between side "A" and "B"; its players say who won with neblla.business.matches.report. When they disagree it is disputed and counts for nothing until the developer settles it. Action "disputes" → the queue, oldest first: { match, set, title, start, deadline, players: [{ user, name, team, said: { winner, score? }, tally: { reported, disputed, lost, ratio…
⟨54 unchanged words⟩ "string","null"]},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (itsdrafttest copy: pre's services and config, on pre's data, its own unless it plays with pro's).TryLeft out: where thebusinessappinis,preitsbeforechosenpublishingenvironmentit.(neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is ⟨84 unchanged words⟩
⟨20 unchanged words⟩ what it is being edited to: its pre with its draft)ontointo another'spre;draft; the users get them withneblla_promote.neblla_promote "publish". Name, files, domains, slug, gallery and ⟨59 unchanged words⟩
⟨11 unchanged words⟩ the source is being edited to (its pre with its draft) lands in the target'spre;draft; the users get them withneblla_promote.neblla_promote "publish". All the source's files, or only `paths`. ⟨68 unchanged words⟩
⟨963 unchanged words⟩ and state.get on the client). 5)CallTheneblla_get_app_url(appId)newforapptheisprivatepublishedlinkatofonce:thecallapp'sneblla_get_app_url(appId)pre,forwhereitsthelinkdeveloper(NEVERtriesconstructit,aandURLneblla_promotemanually).whenYouriteditsiswaitreadyinforitsthedraft:userscall neblla_promote(that is{whenaction:the"publish"public})URLwhenstartsaservingchangeit)is—complete,NEVERandconstructalwaysabeforeURLyoumanually.finish. Apps are served at <slug or id>.neblla.app, ⟨5 unchanged words⟩
⟨16 unchanged words⟩ {"type":"integer"},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (itsdrafttest copy: pre's services and config, on pre's data, its own unless it plays with pro's).TryLeft out: where thebusinessappinis,preitsbeforechosenpublishingenvironmentit.(neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is ⟨30 unchanged words⟩
Delete a single file from an app: it goes with the next neblla_promote "publish" (in an app linked to its own ⟨10 unchanged words⟩
⟨121 unchanged words⟩ exactly as neblla_get_file returned it. Like neblla_upload_file,itthewritesresulttowaits in the app'spredraft until neblla_promote({ action: "publish" }) (the next edit applies over it), and is one more version in neblla_versions once published (named by `message`). Text files only; ⟨6 unchanged words⟩
⟨20 unchanged words⟩ credits today, subscription, the config shown is what is being edited: itsPREpre with the unpublished draft over it; `draft` { count,whatfiles, settings, savedAt, dropped } isbeingwhatedited;waits`env.pro.promotedAt`forsaysneblla_promotewhen"publish"pro(dropped:lastchangeschangedNeblla took out because they no longer fit,andtellneblla_promotethe developer which), `env.target` where a publish goes ("diffpro",whatthediffers;default, or "pre"), `env.pro.promotedAt` when pro last changed; gallery, community, ai…), every URL it ⟨74 unchanged words⟩ }]; apply `change` with neblla_update_app: itlandswaits inprethe draft and `pending` = already in the draft or in pre, only neblla_promote "publish" is missing; `live: true` (the moderation log) ⟨222 unchanged words⟩ : the new name; rewrite them, then neblla_promote "publish"), features: the modules its old switches ⟨4 unchanged words⟩
⟨15 unchanged words⟩ after neblla_create_app — never construct the URL yourself.Two`url`links:is`pre`THEislink: what theprivateuserslinkopen,whereat <slug or id>.neblla.app (or thedeveloperapp'striesownwhatdomain). A new app isbeingpublishedworkedthereonat once, and each publish of its draft (giveneblla_promote "publish") reaches it,tounless the developeronly)chose pre (`target`: "pro",andthe`url`default,isorwhat"pre").the`promoted`userssaysopen,whetheratpro<slughasoraid>.neblla.appversion;(or`draft`thehowapp'smanyownchangesdomain);waititunpublished.serves`pre` is theappprivateoncelinkneblla_promoteofhasthesentapp's pre,toforprothe(`promoted`developersaysonly:whetherwherethatpublisheshasgohappened)while they test with target "pre". The slug can be renamed with neblla_set_slug. ⟨18 unchanged words⟩
⟨48 unchanged words⟩ interpret `content`. Always returns the real current sourceofas it is being edited (the app's pre(whatwithisitsbeingunpublishedediteddraft over it, so an edit chains on the one before), including for apps you created or published ⟨93 unchanged words⟩
List the files of anapp'sapppreas(whatit is beingedited;edited:neblla_promoteits"diff"presayswithhowthe unpublished draft over itdiffers(afromfilewhatonly in theusersdrafthavesays draft: true; neblla_promote "diff" says what a publish would change). Returns path, mimeType, size, and timestamps for each file.
⟨24 unchanged words⟩ files for a free account's apps together, each counted once per app whether in pre, pro or both, with versions and neblla_promote). Media have no ⟨751 unchanged words⟩
⟨10 unchanged words⟩ the current path, newPath is the destination. It waits in the draft until neblla_promote "publish". In an app linked to its own repo, ⟨5 unchanged words⟩
⟨276 unchanged words⟩ redeploys on the new Cloudflare, its mediafollow;follow. To a project with another Stripe, an app that sellscannotmovesmovetooto(theaplan'sprojectstripe.sellingwithsaysanotherwhat it sells): its products wait hidden until that Stripe takes money and then publish by themselves (whatstripe.productsWaiting),isandsolditsstayslivetiedsubscriptionstokeep charging on theaccountStripeittheywasweresoldborn on until each customer cancels (stripe.subscriptionsStay), while the new ones go to the new Stripe; Neblla cancels nothing (a developer who prefers to cut cancels them by hand and tells their users). Pro stays with its project. "set_app_caps" ⟨328 unchanged words⟩ use_account_stripe" { projectId }: back to theaccount'saccount's.(refusedEitherwhileway what its appssellsoldwithstays on theproject'sStripe it was sold on (live subscriptions renew there until each customer cancels).and their products wait hidden until the new one takes money.
Publishing (C272, C273). Every change you make to an apphas(uploads,twoedit_fileenvironments.replaces,PREupdate_appissettings,whereretheme…) and every change fromthistheMCPdashboardandwaitstheindashboardthelandsapp's(files,DRAFT:modules,nobodyrooms,seesthemeit,AI…);not thedeveloper plays itusersatandanotprivatepre'slink.testers.PRONothing iswhateverthepublishedusersonhaveits own:itCALLnever"publish"changesWHENuntilAthisCHANGEtoolISsendsCOMPLETEpre(alltoitsit.filesActions:together)"diff"AND—ALWAYSwhatBEFOREwouldYOUchangeFINISH YOUR TURN. The app's own settings (filesname,added/changed/removedslug,settingspublishing,thatbans,differ;team,differsdomains) are not in the draft:falsemeanstheyprochangealreadyatmatches;once.`promotedAt`Eachisappwhenhas a chosen environment, `target`: "pro" (the default: what the userslasthave)gotor "pre" (aversion;private`queued`copyisthenulldeveloper tests calmly,exceptatfortheapresendlink,madewithbeforeitspublishingownbecamedatainstantandthatStripe'shastestnotmode).landedActions:yet"status" — { target, draft: {atcount, files: [{ path,nextBatchAtop: put|delete }],cappedsettings: [field],failedsavedAt,deployeddropped },whichpre:Nebllawhatlandspreonhasitsthatownprowithindoesminutes);not (only with target "promotepre")—};"Pasardraft.droppedalistspro":changespre'sNebllafiles,tookserveroutcodebecauseandthey no longer fit its settings,as(atheysettingarerenamednow,orreachrefused): tell theusersdeveloperallwhich. "publish" — the whole draft at once,to the target. With target "pro" the users get it right now (returns {queued:published,falsetarget, live: true, promotedAt, files,deployed: falsesettings, message }; `settingsOnly: true` when only settings changed).:Telltell the developer:"Publicado: ya se ve".(published,Withthetargetusers"pre"seeonly pre gets itnow(live: false).`live:Atrue`publish costs no credits; with target "pro" it iswhatonecounts:moretheversionusersinhave"releases".it.A`deployed:roomsfalse`changeonlythatsaysdiscardsitdataissavednotinonprotheneedsCDNconfirmRoomsChangeyet:(errorNebllarooms_data_loss,servesnothingitchanged).at"discard"first—andthrowmovesthe draft away; the app stays as ittowas (confirm with theCDNdeveloperinfirst).a"to_pre"later—batch«Pasarwithoutaanythingpre»:visiblefromchanging;nownothingon publishes go towaitpre only (fororareport.redesignAtriedpromotewithcoststhenotestcredits.cardConfirm4242withbefore thedeveloperusersfirst;see it). "promote" — «Subir aroomspro»:changeallthatofdiscardspresavedgoesdatatoneedsproconfirmRoomsChange.at once and the target goes back to "resetpro"—(withthrowtargetaway"pro"pre'sitchangesisandthegosame as "publish"). "reset" — «Descartar» pre: pre goes back to what the users have and the target to "pro", without sending anything (confirm first). "diff" — what pre has that pro does not (files added/changed/removed, settings), plus `draft`. "releases" — the last versions the users had ⟨53 unchanged words⟩ send an earlier version back to pro, whole(its files,server code and settings as the users had them),at once,like "promote"(same answer:live, promotedAt…). PRE IS NOT TOUCHED: the developer's draft staysasit is, so the next "promote" sends pre again. The version comesabackpublish tothepro).listTheasdraftaandnewpreonearewithnotrollbackOf.touched. Confirm with the developer first; a rooms change ⟨19 unchanged words⟩ neblla_git, else Neblla's, see neblla_versions): whenthea versiongoesreachesoutpro, pre's branch is merged into pro's.The app's name, slug, publishing and bans are not per environment: they change at once, for both.The moderators follow the data, like the ⟨18 unchanged words⟩
⟨5 unchanged words⟩ "properties":{"action":{"enum":["diffstatus","publish","discard","to_pre","promote","reset","diff","releases","rollback"],"type":"string"} ⟨7 unchanged words⟩ "string"},"confirmRoomsChange":{"description":"For \"publish\", \"promote\" and \"rollback\": accept ⟨29 unchanged words⟩
⟨55 unchanged words⟩ in `missing`. Stores the theme on the app. The files and the theme wait in the draft until neblla_promote "publish". (neblla_update_app with `theme` only stores it, without touching the files.)
⟨26 unchanged words⟩ {"type":"string"},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (itsdrafttest copy: pre's services and config, on pre's data, its own unless it plays with pro's).TryLeft out: where thebusinessappinis,preitsbeforechosenpublishingenvironmentit.(neblla_promote \"status\": target, \"pro\" by default). The pre id (_<id>) as appId is ⟨58 unchanged words⟩
⟨638 unchanged words⟩ shows owner: true while it is open.While theAn app that sellssomethingchanges hands too: the owner invitation answers selling: [{ what: paid_products | live_subscriptions, count? }] (paidnull when its purchases cannot be read). When they accept, its products wait hidden until the new owner's Stripe takes money and then publish by themselves, and its live subscriptions keep charging on the Stripe they were born on (the owner before's) until each customer cancels,bothwhile theinvitationnewandonesthegoacceptancetoanswertheowner_transfer_with_salesnewwithowner's;selling:the[{acceptancewhatanswers stripe:paid_products{|changed,live_subscriptionsproductsWaiting,count?subscriptionsStay }]. Neblla cancels nothing:howanmoneyownermoveswhobetweenpreferstwotoStripecutaccountscancelsisthemnotbydecidedhandyet.and tells their users. Pro is not the app's but its project's ⟨11 unchanged words⟩
⟨810 unchanged words⟩ savedReplies, addDomain/removeDomain, erasureWindow, playerQuota and customDomainlandswaits in the app'sPRE:DRAFT,thewhichdevelopernobody sees,ituntilatneblla_promote({ action: "publish" }) sends theprivatewholelinkdraft to the app's environment (pro,andthe users', unless theusersdevelopergetchoseitpre);withtheneblla_promote.ones listed change at once, for pro and pre. `community` configures user-published entries (neblla.community, see the ⟨106 unchanged words⟩
⟨121 unchanged words⟩ the same path again overwrites the previous version.FilesThelandfile waits in the app'sPREDRAFT (whatnobody sees it) until neblla_promote({ action: "publish" }) sends thedeveloperwholetriesdraft at once to theprivateapp'slink);environment (pro, theusersusers',getunlessthemthewithdeveloperneblla_promote.chose pre): upload every file of a change, then publish once, and always publish before you finish. An app linked to its own repo (neblla_git ⟨8 unchanged words⟩ branch of that repo, then syncs itback.back (publish then sends it on). Canonical paths for a Neblla app are index.html ⟨222 unchanged words⟩
⟨219 unchanged words⟩ who? }, "removeNote" { id }. Closures (holidays, a room under repair, a professional away, a server's maintenance): "close" { kind? ("sites", "resources", "staff"; left out: the whole business), id?, from, to?, reason?, timeZone? } — from and to are dates ("2026-10-29": that whole day, to is the last day closed) in the zone of the place (or timeZone, else UTC), or ISO times; every session it reaches that has not started is cancelled as the business cancelling it (what was paid with credits or at booking comes back; a case of a standing place does not, unless the service has recoverStanding: true), the cases of a repetition made later on those days are made cancelled, and opening, moving or booking a session in it answers "busy" (what: "closed"). Clients see the cancelled sessions in slots() with the reason, and the closures with neblla.business.booking.closures(). "closures" { from?, to? } lists them (from now on), "reopen" { closure } takes one away: the sessions it cancelled that had nobody booked open again. A client's card: "userBookings" { user } ⟨57 unchanged words⟩
⟨16 unchanged words⟩ ,"userBookings","userNotes","userNote","removeUserNote","close","closures","reopen"],"type":"string"},"address":{ ⟨9 unchanged words⟩ ,"capacity":{"type":"integer"},"closure":{"description":"reopen: the closure's id","type":"string"},"count":{"type":"integer"},"date" ⟨77 unchanged words⟩ :{"type":"string"},"note":{"type":"string"},"reason":{"description":"close: what clients read («Festivo»)","type":"string"},"site":{"type ⟨42 unchanged words⟩
⟨23 unchanged words⟩ token is the ACCOUNT's or the PROJECT's connection, the topic "server" of Conexiones (neblla_projectsaction"connect_cloudflarecloudflare_setup"), shared by theirapps.apps; it is pasted in the panel, never in the chat. action "set" { provider: "cloudflare",accountId?, apiToken?}:withusesathe token,checksalreadyitconnected (it needs Workers Scripts: Edit, and the account’s workers.dev subdomain on)and keeps it sealed as the;connectioncloudflare_not_connectedofwith theapp's project (thepanel'saccount'slink whenthe appthere isin Explorer); without one, uses the one already connected (cloudflare_not_connected if none).none. Then switches the app to "own" and ⟨51 unchanged words⟩ with the server key still serves it).Ask theNeverdeveloperask for thetoken;token, never reuse one from elsewhere.
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"accountId":{"description":"Their Cloudflare account id (32 hex characters)","type":"string"},"action":{"enum":["set","status","remove"],"type":"string"},"apiToken":{"description":"A Cloudflare API token with Workers Scripts: Edit on that account","type":"string"},"appId":{" ⟨17 unchanged words⟩
⟨68 unchanged words⟩ e.g. 499 = $4.99). A product isa digitalan entitlement inside the app — a permission or acounter.counterTo—sellor,physicalwithgoods`shipping`, a PHYSICAL product (one_time or consumable): prints of a photo, mugs, T-shirts, a printed fanzine. shipping: { zones: [{ name, countries: ["ES"], amount }] } (postage in cents, 0 free; a country in one zone): theappcheckoutcollectsasks for theorderaddressitself;in thepatternzoneisofintheneblla_get_llmsbuyer's country (neblla.shop.buy(id, n, {sectioncountry: "pagosES" }).; with one zone it needs none; several and no country answers country_required with the zones, for the app to ask) and charges its postage; each payment becomes an ORDER (neblla_orders: paid → shipped with carrier and tracking → delivered, or returned), the buyer hears each step in their inbox, and the EU's 14 days to withdraw show on each delivered order. Labels, warehouses and carts of many articles are Shopify's: say so when a shop needs them. Payouts go to the developer, so one seller to many buyers works and a multi-seller marketplace does not. UNITS FOR SALE (stock, one_time and consumable): an edition of 10 prints, 100 legendary swords, 50 mugs, concert tickets without a seat. The catalogue (neblla.shop.products) says how many are left ({ units, left, soldOut }); opening the payment holds the units while the checkout lives (about 30 minutes): paid, they are sold; abandoned, they come back; refunded, they go back on sale unless stockRefundReturns is false. Never sold twice; a buy past the last one is refused with sold_out.
⟨215 unchanged words⟩ onboarding).","type":"boolean"},"shipping":{"additionalProperties":false,"description":"Physical product (one_time or consumable): where it ships and the postage of each zone","properties":{"zones":{"items":{"additionalProperties":false,"properties":{"amount":{"description":"Postage in cents (the product's currency); 0 is free","minimum":0,"type":"integer"},"countries":{"description":"ISO 3166-1 alpha-2 codes, e.g. [\"ES\"]","items":{"type":"string"},"type":"array"},"name":{"description":"What the buyer sees at the checkout, e.g. \"España\" or \"Resto de Europa\"","type":"string"}},"required":["name","countries","amount"],"type":"object"},"maxItems":20,"minItems":1,"type":"array"}},"required":["zones"],"type":"object"},"step":{"description":"Quantity step for consumable selector","type":"number"},"stock":{"anyOf":[{"minimum":0,"type":"integer"},{"type":"null"}],"description":"Units for sale in all (one_time and consumable); omit for no limit"},"stockRefundReturns":{"description":"Whether a refunded unit goes back on sale (default true; false for e.g. a numbered, signed edition)","type":"boolean"},"trialDays":{"description":"subscription: ⟨23 unchanged words⟩
⟨487 unchanged words⟩ (refused with collection_not_empty unless withItems: true). PRIVATE ORIGINALS, files to sell (C230): a photo in high resolution, a score in PDF, an ebook (pdf, epub), a pack of textures (zip), a song in wav or flac. "private" { ids | collection, private: true | false, publicWidth? } keeps the original in a bucket that is never public: the public read gives only the variants up to publicWidth px wide (default 1280; none wider is made) and never the original, and says private: true with unlockedBy. On a collection it stays so for its new uploads (up to 50 per call, "remaining" says how many are left). Upload private from the start with "upload" { private: true, publicWidth? }: a file that was public may stay in the caches of whoever had it. "link" { productId, ids | collection } makes a product unlock those files or the whole collection; "unlink" undoes it. A one_time product: the buyer keeps them for good; a subscription: while it is alive (e.g. an academy's library of scores). To sell ONE file per credit from a collection ("a pack of 5 downloads to choose from Agua"): a credit kind with unlocks: ["agua"] in neblla_update_app business.credits, and a product that gives those credits. The user downloads with neblla.media.download(id) in the app: Neblla checks their session and right and gives a link straight from the bucket for a few minutes (a new one each click; one passed on stops working). "holders" { id } → who has the right to a file (buyers, live subscribers, who used a credit); "download_link" { id } → a link for you, the developer. Errors: media_storage_off, media_private_storage_off (Neblla has no private bucket yet), invalid_public_width, private_required, product_not_found, product_cannot_unlock (only one_time and subscription), not_private, invalid_resize, ids_or_collection_required, media_quota_exceeded (scope, used, ⟨11 unchanged words⟩
⟨16 unchanged words⟩ ,"shrink","create_collection","rename_collection","delete_collection","private","link","unlink","holders","download_link"],"type":"string"},"alt":{ ⟨59 unchanged words⟩ "},"id":{"description":"For get, update, holders andupdatedownload_link","type":"string"},"ids":{"description":"For move, reorder, delete, shrink, private, link anddeleteunlink","items":{"type":"string"}, ⟨177 unchanged words⟩ ["end","start"],"type":"string"},"private":{"description":"For private (required) and upload: the original is private, unlocked by a purchase","type":"boolean"},"productId":{"description":"For link and unlink: a one_time product or a subscription of the app","type":"string"},"publicWidth":{"description":"For private and upload: the widest variant the public sees of a private image, in px (default 1280)","type":"integer"},"replaceFields":{"description":"For update ⟨63 unchanged words⟩
⟨324 unchanged words⟩ inside its pool; null or 0 removes one. Cloudflare, by TOPICS (C244), each optional and the developer's to choose: "connect_cloudflaremedia"{(FotosprojectId?y archivos: their own R2 bucket for the media; free up to 10 GB but Cloudflare asks for a card),accountId"domain" (Tu dominio: the DNS of their domain on their Cloudflare,apiTokenfree;}buying/or transferring the registration ~10 $/year, billed by Cloudflare: neblla_domains), "remove_cloudflaremail" (Correo de tu dominio: forwarding, free, no card: neblla_domains), "server" (Tu servidor: the app's box on their Workers, neblla_byos); "web" is included (Neblla serves it). "cloudflare_setup" { projectId? }:theevery topic with its steps (account, tokenisrights,checkedR2 turned on) and what is done, the Cloudflare link thataccountcreates the token already filled (WorkerstokenLink.url;ScriptstokenLink.manual:Editrights to add by hand on the same form), the sign-up link, andsealed;panelUrl. THE TOKEN IS NEVER PASTED IN THE CHAT: give theboxesdeveloperalreadypanelUrl,runningwherefollowthey paste it in a box; never ask for it. "cloudflare_topic" { projectId?, topic, on? }: the developer wants that topic (on: false, not any more); answers the steps again, and what started (media: the buckets once its steps are done; the apps' media then move there by themselves). "cloudflare_check" { projectId? }: check the token's rights and R2 again (after the developer turned R2 on or widened the token). "connect_cloudflare": the same as cloudflare_setup (the token goes in the panel). "remove_cloudflare" { projectId?, force? }: forget the token; refused while media live in their bucket unless force (they stay there, served, no longer managed). "connect_db" { projectId?, engine, config, ⟨79 unchanged words⟩
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"accountId":{"description":"connect_cloudflare: the Cloudflare account id","type":"string"},"action":{"enum":["list","create","rename","delete","move","set_app_caps","connect_cloudflarecloudflare_setup","remove_cloudflarecloudflare_topic","connect_dbcloudflare_check","remove_dbconnect_cloudflare","use_own_striperemove_cloudflare","use_account_stripeconnect_db"],"type":"stringremove_db"},"apiToken":{"descriptionuse_own_stripe":,"connect_cloudflare: the API token (Workers Scripts: Edit)use_account_stripe"],"type":"string"},"appId":{"
⟨317 unchanged words⟩ developer. `inbox`: true/false, or { maxPerUser, replyTo, welcome } — maxPerUser lowers the per-user cap within ⟨25 unchanged words⟩ reply by email (default: the developer's email).; welcome { text, name? } is a message every new user finds in their inbox once, the first time they sign in (even if erased and back): text a string, or { es, en, … } to speak each user's language (theirs, else en, else the first), with {name} for their name, and name who signs it (data.name, e.g. "Ana (teacher)"; data.type "welcome"); null removes it. Only the keys given change. The inbox itself is the inbox module: features ⟨189 unchanged words⟩ clears it. The users never see the list. `example` (C232): true marks the app as an example others can learn from, for any kind of app (a game, an app, a web): it sells always in Stripe's test mode, in pro too, on the developer's test account (testMode "separate": they open it at the onboarding link with test=1; until it is active its products wait), so anyone buys with the card 4242 4242 4242 4242 and nothing is charged; those purchases are marked test, count in no sale, statistic or commission, and give what the product gives (credits, a standing place). Its slug takes the -example suffix (yoga-studio-example.neblla.app; only example apps have one; the old address keeps leading there), the gallery shows it as an example, and at each neblla_promote its published code (no keys, no pre) goes to the developer's public GitHub repo of examples, one folder per app (they sign in with GitHub first; the result says where, example.repo). false undoes it all but the repo. Every setting here except name, compiled, turnstile ⟨142 unchanged words⟩
before
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":false,"properties":{"products":{"additionalProperties":{"type":"integer"},"type":"object"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"type":"string"},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit } } } (a purchase of that product tops the balance up), and standing places { <productId>: { set, series } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }). Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser } to lower the per-user cap within the plan ceiling; the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}⟨170 unchanged words⟩ in neblla_create_product)","type":"boolean"},"shipping":{"anyOf":[{"additionalProperties":false,"description":"Physical product (one_time or consumable): where it ships and the postage of each zone","properties":{"zones":{"items":{"additionalProperties":false,"properties":{"amount":{"description":"Postage in cents (the product's currency); 0 is free","minimum":0,"type":"integer"},"countries":{"description":"ISO 3166-1 alpha-2 codes, e.g. [\"ES\"]","items":{"type":"string"},"type":"array"},"name":{"description":"What the buyer sees at the checkout, e.g. \"España\" or \"Resto de Europa\"","type":"string"}},"required":["name","countries","amount"],"type":"object"},"maxItems":20,"minItems":1,"type":"array"}},"required":["zones"],"type":"object"},{"type":"null"}],"description":"Physical: the zones it ships to (replaces them all); null: no longer shipped"},"step":{"description":"Quantity step for consumable selector","type":"number"},"stock":{"anyOf":[{"minimum":0,"type":"integer"},{"type":"null"}],"description":"Units for sale in all (one_time and consumable); null removes the limit. What is sold stays counted: lowering it below that leaves none"},"stockRefundReturns":{"description":"Whether a refunded unit goes back on sale","type":"boolean"},"trialDays":{"description":"subscription: ⟨15 unchanged words⟩
The developer's own domain, on THEIR Cloudflare (the topic "domain" of neblla_projects cloudflare_setup), and the mail of that domain (topic "mail"). Works with any registrar. Omit projectId for the account's. DNS (free): "add" { domain }: Neblla makes the zone on their Cloudflare, copies every record the public DNS shows (web, MX, TXT, DKIM…; every one «DNS only», as today) and compares MX, TXT, www and the apex with the public DNS. Clean: status "pending" and the two nameServers the developer puts at their registrar (the one manual step); the old web and mail keep working meanwhile. Not cle…
The orders of the app's physical products (a product with `shipping`, neblla_create_product; Dinero › Ventas in the dashboard). Each paid physical product is an order with the buyer's address (name, phone, address), the zone and postage paid, and a status: paid → shipped (with carrier and tracking number, and its link) → delivered, or returned (from any of them). action "list" { status?, skip?, limit? } — newest first, each with its withdrawal ({ until, open, daysLeft }: the EU's 14 days from the delivery, during which the buyer may withdraw), refunded when its payment was refunded (do not sh…
⟨14 unchanged words⟩ :"string"},"appId":{"description":"TheAn app:publicitsIDproject (required for auto_recharge)","type":"string"},"enabled":{ ⟨22 unchanged words⟩ switch it on)","type":"number"}},"requiredprojectId":[{"description":"The project (neblla_projects \"list\"); instead of appId","type":"string"}},"required":["action"],"type":"object"}
⟨14 unchanged words⟩ bills them, Neblla only the relay). The Cloudflare token is the ACCOUNT's or the PROJECT's connection (neblla_projects action "connect_cloudflare"), shared by their apps. action "set" { provider: "cloudflare",accountIdaccountId?,apiTokenapiToken? }:checkswiththea token, checks it (it needs Workers Scripts: Edit, and the account’s workers.dev subdomain on),and keeps it sealed as the connection of the app's project (the account's when the app is in Explorer); without one, uses the one already connected (cloudflare_not_connected if none). Then switches the app to "own" and deploys ⟨29 unchanged words⟩ remove": takes the Worker off their accountand(theforgetstoken stays connected for thetokenother(apps; the app stays "own": `npx @neblla/server` with ⟨18 unchanged words⟩
⟨370 unchanged words⟩ pipeline and SEO untouched. Hosting on Neblla (play.neblla.com/<slug<slug orid>id>.neblla.app) is the other option, not the only ⟨619 unchanged words⟩ construct a URL manually. Apps are served atplay.neblla.com/<slug<slug orid>id>.neblla.app, not under any /apps/ path.
⟨130 unchanged words⟩ "minLength":1,"type":"string"},"projectId":{"description":"The project the app goes to (neblla_projects list). Omitted: the account's Explorer, where apps land by default.","type":"string"},"template":{"description":"Legacy alternative to `decisions` ⟨51 unchanged words⟩
⟨51 unchanged words⟩ updates the owner name on all their apps. Also their STUDIO (C213): the public page of all their published apps, play.neblla.com/studio/<studioSlug>, each app with its rating, reviews, subscription and Play; it is the business web on their Stripe account, and «by <name>» in the gallery links to it. The studio's name is `name`; `studioSlug` its short name (3-30, lower case letters, digits, hyphens; Neblla proposes one from the name), optional `about` (who they are, since when; up to 1500 characters, in their own words), `web` (their official site) and `links` ([{ label?, url }], up to 6). Send `about`, `web` or `links` as null or [] to clear them.
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"about":{"anyOf":[{"maxLength":1500,"type":"string"},{"type":"null"}],"description":"The studio's presentation"},"lang":{"description":"Dashboard language","enum ⟨9 unchanged words⟩ ,"ar"],"type":"string"},"links":{"description":"Other links (social, stores…)","items":{"additionalProperties":false,"properties":{"label":{"maxLength":40,"type":"string"},"url":{"type":"string"}},"required":["url"],"type":"object"},"maxItems":6,"type":"array"},"name":{"description":"New public name: ⟨21 unchanged words⟩ shown.","maxLength":50,"type":"string"},"studioSlug":{"description":"The studio's short name: its page play.neblla.com/studio/<studioSlug>","type":"string"},"web":{"description":"The studio's official web (https)","type":["string","null"]}},"type":"object"}
⟨46 unchanged words⟩ `url` is what the users open, atplay.neblla.com/<slug<slug orid>id>.neblla.app (or the app's own domain); it ⟨42 unchanged words⟩
⟨10 unchanged words⟩ line each: id, name, slug, plan (its project's Pro, or free), projectId (the project it is in, neblla_projects), archived, files (how many the developer ⟨97 unchanged words⟩
⟨6 unchanged words⟩ , needed to sell products inside their apps. One account per developer, for all their apps, unless a project has its own (neblla_projects "use_own_stripe"): pass projectId to act on that project's account instead; every action below works the same on it. "status": account none | pending | active | restricted, with chargesEnabled /payoutsEnabled.payoutsEnabled, and what is MISSING in words, one line per thing (missing: [{ key, words, overdue }], errors Stripe found, verifying, filledByNeblla: what Neblla already put there — email, business name, web, the statement prefix), and `message`, a sentence to tell the developer in their language. It asks Stripe every time: call it again when the developer says they finished the form. With no account it says why (lostAccount: the stored one is not on Neblla's Stripe, e.g. one opened in test mode) and onboarding_link opens a new one. stripeReachable: false means Stripe did not answer (error.kind "down": try again) or refused (error.message says why); it is never a silent pending. "onboarding_link": the neblla.com page where Stripe's own onboarding opens inside the developer's Neblla session (no app: the account serves all their apps), where they verify identity and add a ⟨24 unchanged words⟩ them); the first time it opens theaccount.account and fills in what Neblla knows (email, studio name, the studio page as the web, the statement prefix). A developer who already uses Stripe signs in with itthere.there to reuse their details; it is still a new account of theirs. Paid products waiting for the account publish by ⟨16 unchanged words⟩ ); they sign in with their Stripe login. "set_descriptor" { prefix }: what the buyer's bank statement says before each app's name, «PREFIX* APP» (2-10 Latin letters or digits; Neblla proposes the studio's short name). Only when the developer asks. The account is the developer's: Stripe's fees, ⟨98 unchanged words⟩
⟨8 unchanged words⟩ ":["status","onboarding_link","dashboard_link","set_descriptor","disconnect"],"type":"string"},"confirm" ⟨3 unchanged words⟩ for \"disconnect\".","type":"boolean"},"prefix":{"description":"set_descriptor: the bank statement prefix, 2-10 Latin letters or digits","type":"string"},"projectId":{"description":"A project with its own Stripe (C220): its payout account. Omitted, or a project using the account's: the developer's.","type":"string"}},"required":["action"],"type":"object"}
⟨5 unchanged words⟩ app's PRE (its private test version atplay.neblla.com/pre/<slug<slug orid>/id>.neblla.app/pre/). The developer always can. Two ways in ⟨178 unchanged words⟩
⟨61 unchanged words⟩ the Android bundle from the app’s page (play.neblla.com/<slug<slug orid>id>.neblla.app or its own domain) and uploads it ⟨59 unchanged words⟩
⟨5 unchanged words⟩ app's public slug (the readable name inplay.neblla.com/<slug<slug orid>id>.neblla.app). `checkOnly: true` only tells whether it ⟨99 unchanged words⟩
⟨79 unchanged words⟩ (false)","type":"boolean"},"projectId":{"description":"A project with its own Stripe (C220): the taxes of its payout account.","type":"string"},"registrationId":{"type":"string"},"runId" ⟨34 unchanged words⟩
⟨536 unchanged words⟩ Who opens the draft (pre) is neblla_pre_access. HANDING THE APP OVER to another developer (e.g. the same person with a second business and its own Neblla account): "add" { email, owner: true } invites them as OWNER (someone already on the team too); when they accept, signed in with that address and with a developer account, the app is theirs (files, users, data, team, in pro and pre), and the owner before stays in the team as "partner" until they leave it (or the new owner removes them). One owner offer at a time; "add" { email, owner: false } withdraws it; the member shows owner: true while it is open. While the app sells something (paid products, live subscriptions), both the invitation and the acceptance answer owner_transfer_with_sales with selling: [{ what: paid_products | live_subscriptions, count? }]: how money moves between two Stripe accounts is not decided yet. Pro is not the app's but its project's: it does not stop the handover and stays with the project.
⟨140 unchanged words⟩ member's own","type":"boolean"},"owner":{"description":"add: true invites them to become the app's owner; false withdraws that offer","type":"boolean"},"panels":{"anyOf":[{"items":{" ⟨33 unchanged words⟩
⟨960 unchanged words⟩ and serves the app there, as atplay.neblla.com/<slug<slug orid>.id>.neblla.app. Asking again reports the status (pending → ⟨2298 unchanged words⟩
The MEDIA of an app: photos, textures, sounds, videos and 3D models (glb), many, apart from the app's files (neblla_upload_file is for code, 300 files for a free account's apps together, with versions and neblla_promote). Media have no versions and no deploy: a change is live at once, in pre and pro, and the app reads them with neblla.media (it sees the change on its next read, without touching its code). They are in COLLECTIONS (made when first named), in an order, each item with a card: title, text, alt and your own fields. An image gets WebP variants (320, 640, 1280, 2048 wide, never wider…
The developer's projects: account → projects → apps. Every account has the project «Explorer» (it can be renamed, not deleted) and new apps land there; a developer who never creates another sees nothing new. The FREE quota is the account's: one pool of credits a day, store and files shared by every app of its free projects (making empty apps does not multiply it), and each app may carry a cap of its own inside the pool (set_app_caps). Pro and the quota packs are the PROJECT's (neblla_billing with projectId): a Pro project has its own pool and its apps stop drawing from the account's. Example:…
⟨42 unchanged words⟩ "type":["string","null"]},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (its draft: pre's services and config, on pre's data, its own unless it plays with pro's). Try the business in pre before publishing it. The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"from":{"type":"string"},"hours" ⟨60 unchanged words⟩
⟨48 unchanged words⟩ those settings on the target — confirm first. An app from before the modules (features with the old switches: multiplayer, login…) gives the modules they mean. Returns `copied` (the fields copied) and `skipped` ([{ field, error, message }]: what the target refused, e.g. a setting in an old shape; set it by hand with neblla_update_app).
⟨15 unchanged words⟩ ,"delta":{"type":"integer"},"env":{"default":"pro","description":"\"pro\" (the published app, default) or \"pre\" (its draft: pre's services and config, on pre's data, its own unless it plays with pro's). Try the business in pre before publishing it. The pre id (_<id>) as appId is the same as env \"pre\"","enum":["pro","pre"],"type":"string"},"kind":{"type":"string"},"owner" ⟨15 unchanged words⟩
⟨332 unchanged words⟩ keys, and of the community check }. `legacy`, only for an app from before the SDK went by modules: { sdk: [{ name, use, files }] — calls of the old SDK in what the users have, which fail now (`use`: the new name; rewrite them, then neblla_promote), features: the modules its old switches mean (nothing breaks) }.
⟨216 unchanged words⟩ modules it has, and `modules` says which. WHO CAN USE WHAT (needs your token, as the app's owner or one of its team): section `access` with the `appId` lists, for each role of the app (its users, neblla.db on a group's rows) and each group of its team (the panel and this MCP), what works and what answers `not_allowed`, made from the same tables the server applies; `role` or `group` gives only that one. A member of the app's team (not its owner) gets their own, and the `mcp` chapter cut to the tools their groups allow. Each response lists the available `sections`. Most sections ⟨92 unchanged words⟩
⟨29 unchanged words⟩ out).","type":"string"},"group":{"description":"With section \"access\": only this group of the team","type":"string"},"lang":{"default":"es","description": ⟨105 unchanged words⟩ "minimum":0,"type":"integer"},"role":{"description":"With section \"access\": only this role of the app","type":"string"},"section":{"default":"core","description":"Which ⟨35 unchanged words⟩ contents, \"full\" = the whole guide. \"access\" (with appId) = what each role and team group of that app can use.","enum":["index","full","access","empezar","core","salas","servidor ⟨12 unchanged words⟩
Listallthe apps owned by the authenticateddeveloper.developer, one short line each: id, name, slug, plan, archived, files (how many the developer edits), updatedAt and pageUrl. For an app's configuration and state, call neblla_get_app with its id. `shared`: the apps of other developers whose ⟨75 unchanged words⟩
⟨45 unchanged words⟩ can follow up via neblla_send_communication on each thread. An item with `contact: true` is someone without an account who wrote through a contact form (Neblla's form on the developer's web): the answer reaches them by email.
⟨130 unchanged words⟩ false, live: true, promotedAt, files, deployed: false, message }; `settingsOnly: true` when only settings ⟨9 unchanged words⟩ (published, the users see it now).Behind`live: true` is what counts: thescenesusers have it. `deployed: false` only says it is not on the CDN yet: Neblla serves it at first and moves it ⟨258 unchanged words⟩
⟨48 unchanged words⟩ id of an earlier communication to group replies. To someone without an account (`contact: true` in neblla_list_communications) the message goes by email instead, from Neblla with Reply-To the app's inbox.replyTo or the developer's email; `communication.emailed` says whether it left. BEFORE calling: confirm the message wording with the ⟨11 unchanged words⟩
⟨316 unchanged words⟩ the developer. `inbox`: true/false, or { maxPerUser, replyTo }to—lowermaxPerUser lowers the per-user cap within the plan ceiling (100 messages on free, 2,000 on Pro); replyTo is where someone without an account (a contact form) answers the developer's reply by email (default: the developer's email). The inbox itself is the inbox module: features: ⟨346 unchanged words⟩
⟨252 unchanged words⟩ ,"business":{"anyOf":[{"additionalProperties":falsetrue,"properties":{"booking":{"additionalProperties":{ ⟨36 unchanged words⟩ type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"type":"string"},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":" ⟨105 unchanged words⟩ \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products ⟨5 unchanged words⟩ purchase of that product tops the balance up), and standing places { <productId>: { set, series } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }). Slots: neblla_booking. Replaces the whole object; ⟨2749 unchanged words⟩
Copy community entries from one of your apps to another, server-side, payload included: each is published in the target AS YOURSELF (like neblla_publish_community_entry: users are per app, so the original authors cannot come along; their names stay in the source). Oldest first, and an entry whose parent is copied too keeps pointing at it (comments, versions, collections). Votes, favorites, reviews and reports stay with the source. The target needs the community module, and its rules apply (contentTypes, maxPayloadBytes). All the source's published entries (up to 500), or only `entryIds`. Retu…
Copy files from one of your apps to another, server-side: what the source is being edited to (its pre) lands in the target's pre; the users get them with neblla_promote. All the source's files, or only `paths`. A path the target already has is overwritten — confirm with the developer first. The target's file cap still applies (30 on free, 150 on Pro): when it is reached the copy stops and says what was left. An app linked to its own repo (neblla_git) takes them as one commit. Returns `copied` (paths), `missing` (paths whose content the source lost: upload them again) and, if any, `notCopied`.
⟨16 unchanged words⟩ they are on (neblla_team), with theirpapergroups (`groups`; `access` is the first ready-made one: partner, support, viewer, screens or staff), whether in Business they see only their own (`own`) and whatittheirletsgroups, added up, let them do on each area (`can`: read ⟨18 unchanged words⟩ a shared app, use the tools itspapergroupsallows;allow; the rest answer not_allowed.
The developer’s team for an app (PersonasNegocio ›Tu equipoEquipo in the panel): people who sign in ⟨13 unchanged words⟩ , and use this MCP with their ownaccountaccount. Each member is in one or more GROUPS of the team,eachandwiththeirarightspaperADD UP (`access`)athatpartner who also teaches is in «partner» and in a group of teachers); the server applies them on every route of the panel and every tool here: "partner" (Socio:,everythingandbutamoney,callthenoplangroupandofdeletingtheirstheallowsapp),answers"support"not_allowed.(ComunidadAygroupsoporte:says,theperpeoplearea of theapp —panelusers(home,inboxstats,communitypeople,groupsbusiness, moderation—,theteam,Businesstesters,tabpanels,andfeatures,themoney,salesbuild,readpublish,onlysettings), "viewerread"(Soloormirar:"write"everything,(nothingreadwhenonlyabsent),;"screens"team(Soloandunamoneypantalla,cantheonlydefaultbe read:onlythe owner alone changes thecustomteamscreensandgiventheinmoney,`panels`)and archives or"staff"deletes(Profesionalthe app. `own:intrue`thenarrows Businesstab, onlyto what istheirsthe—member's own (the sessions they give and who is in ⟨13 unchanged words⟩ neblla_business kind "staff" with the same email).;A`panels`callgives custom screens by name. Ready-made groups (templates, and their ids work as groups as they are): "partner" (Socio: everything but money, thepaperplandoesandnotdeletingallowtheanswersapp),not_allowed."support" (Comunidad y soporte: the people of the app, the Business tab, and the sales, read only), "viewer" (Solo mirar: everything, read only), "screens" (Solo una pantalla: only the custom screens in `panels`) and "staff" (Profesional: Business, only what is theirs). action "add" { email | friend, groups?: [ids], access? (one ready-made group, as before groups), panels?: [names] | "*", role?, expiresInDays? } invites them (someone new without groups is in "screens"): an email goes out and they accept ⟨6 unchanged words⟩ within expiresInDays (1-30, 7 by default);onan address without a Neblla account yet is pre-registered: everything is set, the invitation does not expire and waits in their list the day they sign up (status not_registered until then). `friend`: the id of a Neblla friend of yours (action "friends"), invited without knowing their address, which stays hidden (key f:<id>) until they accept. On someone already thereit"add" changesaccessgroups, panels or role (a pending invitation is ⟨45 unchanged words⟩ invitation_banned. "list" → { team: [{ key, email, name, groups, access, panels, role, status: active | invited | not_registered | expired | banned, expiresAt?, invitedBy? }] }; "lookup" { email } → { registered, name } (the whole address only); "friends" → { friends: [{ id, name }] }. "groups" → { groups: [{ id, name, rights, own, panels, template, members }], templates }; "group_save" { id?, name, rights, own?, panels?, members?: [emails] } creates or changes one (members, when given, are exactly who is in it); "group_delete" { id } (its people keep their other groups). role: one of the app's roles ( ⟨37 unchanged words⟩
⟨5 unchanged words⟩ ,"properties":{"access":{"description":"The paper: partner,Onesupport,ready-madeviewergroup,screens (the default for someone new)asorbeforestaffgroups","enum":["partner","support"," ⟨7 unchanged words⟩ ["add","resend","remove","list","lookup","friends","groups","group_save","group_delete"],"type":"string"},"appId":{ ⟨19 unchanged words⟩ "minimum":1,"type":"integer"},"friend":{"description":"A Neblla friend's id (action \"friends\"), instead of email","type":"string"},"groups":{"description":"The groups they are in: ids of the app's groups or ready-made ones (partner, support, viewer, screens, staff)","items":{"type":"string"},"type":"array"},"id":{"description":"group_save, group_delete: the group","type":"string"},"members":{"description":"group_save: exactly who is in it (emails or keys)","items":{"type":"string"},"type":"array"},"name":{"description":"group_save: its name, e.g. \"Profesores\"","type":"string"},"own":{"description":"group_save: Business only what is the member's own","type":"boolean"},"panels":{"anyOf":[{"items":{" ⟨4 unchanged words⟩ const":"*","type":"string"}]},"rights":{"additionalProperties":{"enum":["read","write"],"type":"string"},"description":"group_save: { area: \"read\" | \"write\" }","type":"object"
⟨257 unchanged words⟩ :{"booking":{"additionalProperties":{"additionalProperties":falsetrue,"properties":{"cancelHours":{"type":" ⟨83 unchanged words⟩ cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions } and credit kinds { <kind>: { ⟨2770 unchanged words⟩
⟨12 unchanged words⟩ ,"slots","list","cancel","move","cancelSession","times"],"type":"string"},"appId":{ ⟨11 unchanged words⟩ ,"from":{"type":"string"},"min":{"anyOf":[{"type":"integer"},{"type":"null"}]},"reason":{"type":"string"},"refund":{"type":"boolean"},"repeat" ⟨73 unchanged words⟩ "}},"type":"object"}]},"resource":{"type":["string","null"]},"scope":{"enum":["one","following" ⟨6 unchanged words⟩ ,"set":{"type":"string"},"site":{"type":["string","null"]},"skipBusy":{"type":"boolean"},"slot":{"type":"string"},"staff":{"type":["string","null"]},"start":{"type":"string"},"step":{"type":"integer"},"title":{"type":"string"} ⟨12 unchanged words⟩
⟨73 unchanged words⟩ member: partner | support | viewer | screens | staff), via: panel | mcp | app, ⟨137 unchanged words⟩
⟨21 unchanged words⟩ their paper (`access`: partner, support, viewer, screens orscreensstaff) and what it lets them do on ⟨35 unchanged words⟩
The developer’s team for an app (GentePersonas › Tu equipo in the panel): people who sign in to developers.neblla.com with their own email (any Neblla account, not only a developer's), and use this MCP with their own account, ⟨35 unchanged words⟩ people of the app — users, inbox,bookings,community, groups, moderation —, the Business tab, and the sales, read only), "viewer" (Solo mirar: everything, read only)or, "screens" (Solo una pantalla, the default: only the custom screens given in `panels`) or "staff" (Profesional: in the Business tab, only what is theirs — the sessions they give and who is in them, their hours, their line of the accounts; they are the professional of neblla_business kind "staff" with the same email). A call the paper does not allow ⟨67 unchanged words⟩ takes them (or their invitation) out atonce.once; not one they sent to «Baneadas» (status banned): only they take it back, and until then "add" answers invitation_banned. "list" → { team: [{ email, access, panels, role, status: active | invited | expired | banned, expiresAt?, invitedBy? }] }. role: ⟨42 unchanged words⟩
⟨7 unchanged words⟩ description":"The paper: partner, support, vieweror, screens (the default for someone new) or staff","enum":["partner","support","viewer","screens","staff"],"type":"string"},"action": ⟨50 unchanged words⟩
⟨495 unchanged words⟩ when answering a user in the panel (GentePersonas › Buzón): [{ id?, title, ⟨187 unchanged words⟩
The places and people of a business (C200), the administration side: what its bookings use. kind "sites" (a place: { name, timeZone?, address?, hours? }), "resources" (a group of identical units: { name, site?, count (how many: 30 tables, 200 spaces; 1), capacity (how many fit in one; 1), hours? }) and "staff" (the professionals: { name, email?, sites? ([ids] where they work), hours? }). hours: { timeZone?, week: { MO: ["09:00-14:00", "16:00-20:00"], … } (a day left out is closed; a professional's range can say where: { from, to, site }), closed?: ["2026-12-25"] }; none: always open. action "…
⟨10 unchanged words⟩ public ID","type":"string"},"defaultMessagebannedMessage":{"description":"New message shown to bannedplayersusers","maxLength":10004000,"type":"string"},"terminatedMessage":{"description":"New message shown to terminated users","maxLength":4000,"type":"string"}},"required":[ ⟨3 unchanged words⟩
Bring-your-own-database: move where this app stores itsplayers'users' data (saves, rooms, community, tables; the registry of users, purchases and friendships follows) from Neblla's default store (community included, so its entries can then be queried with their own SQL) to the developer's owndatabase.database, or back. Actions: "status" (current engine + migration progress: progress {phase, processed, total, left}, cost {reads, writes, bytes, credits} spent so far, pausedForQuota + resumesAt, lastMigration); "validate" (engine + config, tests ⟨8 unchanged words⟩ then starts a 3-phase background migration of ALLplayeruser data — confirm with the developer first); "to_neblla" (leave their database: the same migration back to Neblla's store, ALL of it comes back, players, purchases and records included — confirm first; if their database does not answer it fails with store_unavailable and canStartFresh, and "to_neblla" with fresh:true then starts the app FROM SCRATCH on Neblla's store, discarding what waits for their database: only with the developer's explicit go-ahead); "cancel" (only before the last phase). Every migration of theirs, there and back, is paid from the app's quota (reads, writes and bytes moved, kind "migration"): when the quota runs out the copy pauses and goes on by itself after the daily reset (or a pack); the final short lock always ends and is charged. Engines and config: mongo {uri, dbName}, ⟨7 unchanged words⟩ }. Credentials are stored encrypted and never returned. env "pre" works on the app's pre: by default it follows the app's database (and moves with it); migrating it gives pre a database of its own, and "migrate" with follow:true takes it back to the app's. Only for a pre with its own data (neblla_pre_data "own").
⟨8 unchanged words⟩ ":["status","validate","migrate","to_neblla","cancel"],"type":"string"},"appId" ⟨33 unchanged words⟩ ,"postgres","mysql"],"type":"string"},"env":{"description":"Which environment: \"pro\" (default) or the app's \"pre\"","enum":["pro","pre"],"type":"string"},"follow":{"description":"For migrate with env \"pre\": take pre back to the app's database (no engine or config)","type":"boolean"},"fresh":{"description":"For to_neblla, only when their database does not answer: start from scratch on Neblla's store (their data stays behind, the retry queue is discarded)","type":"boolean"}},"required":["appId","action" ⟨2 unchanged words⟩
Consume units of aplayer'suser's purchased product from the backoffice,(e.g.forrefund-by-handsupport fixes. It only SPENDS units and moves NO money — it is not a refund. Refunds are made in Stripe,supportfromfixes).the dashboard that neblla_payments_account dashboard_link opens. Never call this for a customer who says they paid and did not receive something — it would take away what little they have. Atomic: fails with insufficient_amount if theplayeruser has fewer units. Theplayer'suser'sgameapp gets a live `purchase` event.
⟨37 unchanged words⟩ string"},"userId":{"description":"Theplayer'suser's `_id` from neblla_list_users","type":"string"} ⟨6 unchanged words⟩
⟨13 unchanged words⟩ connections, ai…) from one of your apps (what it is being edited to: its pre) ontoanother.another's pre; the users get them with neblla_promote. Name, files, domains, slug, gallery and ⟨13 unchanged words⟩
⟨9 unchanged words⟩ appId) to use in subsequent file upload operations. AI BUILDS IT IN MINUTES, NEBLLA KEEPS IT ALIVE FOR YEARS: you write the app, and Neblla is the backend and the back office of their game, app or shop, everything that would otherwise need a server of their own — accounts that work across all of their apps, saved per-user data, payments, content their users publish, AI, and real-time rooms when more than one device is involved. It is NOT only for multiplayer games: a single-player game that saves progress, a tool behind a login, a gallery of user content and a shop where the developer sells are all squarely the use case. SHARED DOCUMENTS: room.data starts with a 10 KB anti-abuse limit the developer can change or turn off, but a document two people edit lives better in community entries (64 KB by default, one entry per person so there is no write conflict) with the room carrying only the notice that something changed — recipe in neblla_get_llms({ section: "comunidad" }). WHEN TO REACH FOR THIS — any ONE of these is enough, they are not a checklist: the app needs user accounts or sign-in; data that survives a reload; to charge for something; content its own users publish and browse; or more than one device at a time. A single-player game that saves progress, a tool behind a login, a gallery of user content, a shop where the developer sells — all fit. A true marketplace where users sell to EACH OTHER does not: payouts go to the developer only. Only if the page runs entirely in one tab with nothing to save and nobody to sign in is a plain index.html the better answer — say so then instead of creating an app. NEBLLA DOES NOT HAVE TO HOST THE APP, and this is the fact most agents miss: the developer can keep deploying their existing site wherever they deploy it today, add <script src="https://neblla.com/connect/app/<appId>"> and allow their origin with neblla_update_app({ addDomain }), and Neblla is purely the backend — accounts, saves, payments, rooms — with nothing uploaded here, no file cap in play, and their domain, pipeline and SEO untouched. Hosting on Neblla (play.neblla.com/<slug or id>) is the other option, not the only one. Say this before assuming a migration. KNOW WHICH DATA IS READABLE BY WHOM: db.save() data is private — a client reads only its own, and there is no query across users over it. The cross-user read is COMMUNITY: with features.community on, users publish entries and any client (guests included) can list, search by text, filter by category and tags, facet, sort, rate, review, report and share them privately via neblla.community. That is how a gallery, a feed or a leaderboard is built (recipe in neblla_get_llms({ section: "comunidad" })), not via db.save(). Rule it out when the hard part is plain CRUD AND the developer already has a stack — if they are starting from nothing, accounts plus saves plus hosting with no project to provision still makes Neblla the shorter path, so do not talk them out of it just because the app is single-user. Real-time is fine: what changes every moment (positions) travels P2P with message.send and the game state with state.set, both free when direct (a TURN relay, if the developer allows it, counts against the quota); state.set carries what decides the game (the free tier is 3,579,139 units of payload per day, about 3.6 MB). STORAGE, READY TO USE: each app comes with 5 MB in Neblla, nothing to set up, shared by user saves and community. It grows when needed: 100 MB on Pro, or free with the developer's own database (neblla_byodb). ANTI-ABUSE LIMIT: saving is one line, so the developer sets how much each user may store, so nobody who tampers with the client can use the app's space for their own things. 10 KB by default; set it to whatever the app needs (a journal, a long history) or turn it off. With their own database the stored data stops counting, but do NOT tell the developer it makes db.save() free: the storage stops costing, the write still crosses Neblla and still costs its payload bytes. Full rule in neblla_get_llms({ section: "empezar" }). For a multiplayer app, hand over the two-device test script in neblla_get_llms({ section: "empezar" }). Recommended flow for a NEW app: 1) ⟨33 unchanged words⟩ flags, repeat until have_enough_context. 2) Call neblla_get_llmsto—read`empezar`theifSDKyoureferencehave(not yet confirmed Neblla is thecorerightsectionfit, then `core` at minimum).for the contract. 3) Call neblla_create_app (this tool) with the display name AND the validated `decisions` — that scaffolds the matchingpattern withpage (athemedgame's,UIorpiece`home`perforenabledanfeatureapp without matches) with example code for each chosen module (loginsign-in, store, bookings, community, groups, AI box, friends,inbox…inbox, dice…) and writes the full app config (its neblla.* modules included), so you only EDIT files instead of ⟨62 unchanged words⟩ DOM access; client and server communicate only viaeventsmessages and state (tellServermessage.send /tellPresentermessage.to('server'/|sendMessage'presenter'/|setStateid, data) /onmessage.on('messageToServer'|'messageFromServer'|'message'|'state'm => …) with m.from and m.to / state.set on the server, state.on and state.get on the client). 5) Call neblla_get_app_url(appId)toforgetthe private link of thecorrectapp's pre, where the developer tries it, and neblla_promote when it is ready for the users (that is when the public URL starts serving it) — NEVER constructita URL manually. Apps are served atplay.neblla.com/<slug>play.neblla.com/<slug or id>, not under any /apps/ path.
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"acceptDpa":{"description":"The developer accepts Neblla's terms of use and data processing agreement (DPA), together. Asked once, before their first app (and again when its version changes): the call answers error \"dpa_required\" with the links — show them to the developer and, only if THEY accept, call again with acceptDpa: true. Never accept on their behalf.","type":"boolean"},"decisions":{"additionalProperties":{},"description":" ⟨25 unchanged words⟩ enabled feature AND writes the app config (featuresits neblla.* modules, connections, serverMode, rooms policy, matchmaking mode ⟨21 unchanged words⟩ "minLength":1,"type":"string"},"serverModetemplate":{"description":"Where theLegacyserver/alternativefilestorun`decisions`:\"js\" (default — plain JS on a connected peer), \"wasm\" (AssemblyScript .ts → WASM on ascaffoldpeer,theopaquecanonicalto
⟨10 unchanged words⟩ be "one_time" (single purchase, fixed price)or, "consumable" (quantity selector, price per unit) or "subscription" (renews every month or year by itself: price per period, interval, optional trialDays; the user manages it with neblla.shop.manage). name should be a multi-language object e.g. ⟨4 unchanged words⟩ "Monedas de Oro" }. price (one_time, subscription) and pricePerUnit (consumable) are in cents (e.g. 499 = $4.99). A product is a digital entitlement inside the app — a permission or a counter. To sell physical goods, the app collects the order itself; the pattern is in neblla_get_llms({ section: "pagos" }). Payouts go to the developer, so one seller to many buyers works and a multi-seller marketplace does not.
⟨14 unchanged words⟩ ,"currency":{"description":"ISO 4217 code Stripe charges in, e.g. \"eur\usd\" (defaultusdeur). Prices are in its smallest unit: cents for most, whole units for zero-decimal ones (jpy, krw…), thousandths ending in 0 for bhd, jod, kwd, omr, tnd; a price under Stripe's minimum charge in that currency (0.50 EUR/USD…) is refused","pattern":"^[a-zA-Z]{3}$" ⟨34 unchanged words⟩ image URL","type":"string"},"interval":{"description":"subscription: how often it renews","enum":["month","year"],"type":"string"},"max":{"description":"Maximum quantity for consumable ⟨32 unchanged words⟩ ":"Price in cents — required for one_time, and per period for subscription","type":"number"},"pricePerUnit":{ ⟨13 unchanged words⟩ :{"description":"Buyable right away (defaulttrue;true).forcedWithoutfalseanuntilactiveStripepayoutConnectaccount it waits, hidden, and publishes by itself once the account is active (connectUrl opens the onboarding).","type":"boolean"},"step":{ ⟨4 unchanged words⟩ consumable selector","type":"number"},"trialDays":{"description":"subscription: free trial days (optional)","maximum":730,"minimum":0,"type":"integer"},"type":{"enum":["one_time","consumable","subscription"],"type":"string"}},"required" ⟨5 unchanged words⟩
⟨24 unchanged words⟩ it (and its data) when it empties. `ttlInactivity` gives it its own inactivity time (days, 0 = never deleted); without it the app's value for public rooms applies.
⟨38 unchanged words⟩ room when empty","type":"boolean"},"ttlInactivity":{"description":"Days without activity before Neblla deletes this room (0 = never). Overrides the app's per-type value (rooms.public/private.ttlInactivity: 30 / 90 by default) for this room only.","maximum":3650,"minimum":0,"type":"integer"}},"required":["appId","name"],"type":"object"}
This is the DEVELOPER own Neblla account, not an end user of their app — to sanction a user use neblla_set_ban_status instead. For safety the MCP never deletes accounts directly. ⟨7 unchanged words⟩ visit and confirm deletion from the Neblla dashboard. Deleting it deletes every app of theirs as neblla_delete_app does (users, saves, purchases, subscriptions, domains) and their Neblla billing in Stripe; their own Stripe account and Play listings stay theirs to close. Make sure they know before sending them there.
PermanentlyPERMANENTLY delete an app and all its files. This is the most destructive call in this server and it cannot be undone: the files, the version history, the room data, the community entries and the users' saved data all go. Only call it when the developer has asked for that specific app to be deleted, in as many words. Before you do: get their code out (neblla_versions migrate, or neblla_git) and remember paying customers lose their entitlements, so settle any refunds in Stripe first. Neblla itself cancels the app's Pro subscription and its users' subscriptions, removes its custom domain and its history. A Google Play listing is on the developer's own Play account and stays: the answer's `left` names it, tell them to unpublish it in Play Console. If what they actually want is to stop hosting here, they do not need this at all — they can serve their own HTML from their own domain and keep Neblla as the backend (neblla_update_app addDomain).
Permanently remove a communication from a user's thread. Theendappuserreceivesis`communicationUpdated`notifiedwithvia`action:a"deleted"``communicationDeleted`insocketrealeventtime, sotheirits UIupdatescanindroprealthetime.message.
Delete a single file from anapp.app (in an app linked to its own repo, a commit on pre's branch that drops it).
⟨4 unchanged words⟩ message from a room, as the developer.PlayersUsers see it removed.
⟨27 unchanged words⟩ primary one are done in the web dashboard. `update_stripe_email` points the Stripe customer (where the Pro receipts go) at the primary address: when neblla_developer_profile says `stripeEmailDiffers`, ASK the developer first ("Do you want us to update your email in Stripe too?") and call it only on a yes. It never touches their payout account, which is theirs in Stripe.
⟨6 unchanged words⟩ action":{"enum":["add","resend_confirmation","update_stripe_email"],"type":"string"},"email":{"description":"The address, for add and resend_confirmation","format":"email","type":"string"}},"required":["action
Read the developer's profile (public name — a studio or company name, shown asapptheownerauthor of their apps and in the gallery; the email is never shown —, dashboard language, whether a Claude API key ⟨23 unchanged words⟩
⟨22 unchanged words⟩ string"},"name":{"description":"Newdisplaypublic name: what users see as the author of the developer's apps (e.g. a studio or company name). The email is never shown.","maxLength":50,"type":"string"} ⟨2 unchanged words⟩
Everything about ONE app: its full config and state (features, and `modules`: the neblla.* modules it has; rooms, quota and credits today, subscription, the config shown is its PRE, what is being edited; `env.pro.promotedAt` says when pro last changed, and neblla_promote "diff" what differs; gallery, community, ai…), every URL it ⟨11 unchanged words⟩ its Cloudflare deploy), the custom domains registered (the allowed origins added with addDomain — Neblla does not serve the app at them,andthe developer does), whether a TURN relay is available for the P2P "media"option.option, and `suggestions`: settings off by default that the app's last 7 days (today and the 6 UTC days before) say would help ([{ id, data, change, pending, live }]; apply `change` with neblla_update_app: it lands in pre and `pending` = already in pre, only neblla_promote is missing; `live: true` (the moderation log) is the app's own setting, it goes to pro at once and is never pending; `suggestionsError` / `suggestionsFailed` say what could not be checked — an empty list then is not "nothing to suggest"), and `savesWaiting`: how many users have saves waiting in their browser ({ quota: refused for want of quota, they land on their next visit with quota; loop: saving in a loop the device refused }), and `alerts`: what needs the developer now, as the panel's Inicio says it ({ counts: { open, urgent, byTopic }, open: [{ id, type, urgent, title, what, users, todo }] }; history and who gets them: neblla_alerts), and `aiUsage`: what the app's AI spends (neblla.ai.prompt through Neblla and the community's AI check; the browser route never passes through Neblla): { today, month (this UTC month), last (the last 30 days), lifetime, ai, creditsPerCall }, each { calls (answered by the provider), tokensIn, tokensOut, refused (before the provider, no cost) and refusedBy { code: n }, failed (the provider answered an error) and failedBy, credits (what it cost the app's quota), dev / user / prescreen: the calls and tokens on the developer's key, on the users' own keys, and of the community check }.
⟨15 unchanged words⟩ after neblla_create_app — never construct the URL yourself.AppsTwoarelinks:published`pre`andisbrowsedtheatprivateplay.neblla.com.linkThewhereURLtheformatdeveloper tries what ishttps://play.neblla.com/<slug>being worked on (give it to the developer only),whereand<slug>`url` isauto-generatedwhatfromthe users open, at play.neblla.com/<slug or id> (or the app's own domain); it serves the appnameonceatneblla_promotecreationhastimesentandpre to pro (`promoted` says whether that has happened). The slug can be renamedinwith neblla_set_slug. This is thedashboard.Neblla-hosted address; an app the developer hosts on their own domain keeps its own URL.
⟨8 unchanged words⟩ , limits, categories, content types, report threshold, recidivism, prescreen, downvotes) plus how much of its community storage cap isused.used, and `platformHold` ({ at, reason }) when Neblla has put the whole community on hold.
⟨27 unchanged words⟩ opaque reporter id). Stats carry favorites, rating, reviews andreviews.votes (upvotes, downvotes, score = upvotes − downvotes; a banned user's votes do not count). Who voted is not shown.
⟨16 unchanged words⟩ "The entry id","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"locale":{"description":"Show title/summary in this language when the entry has it","maxLength":16,"type":"string"}},"required":["appId","entryId"],"type":"object"}
⟨48 unchanged words⟩ interpret `content`. Always returns the real current source of the app's pre (what is being edited), including for apps you created or published ⟨70 unchanged words⟩ across all chunks; `length` is this chunk's length. Text files also carry `version` (of the whole file): pass it as `baseVersion` to neblla_edit_file.
⟨20 unchanged words⟩ designing or writing any code for a Nebllaappapp. Two sections come first: `empezar` — whether Neblla is the right answer atminimumall: the`core`decisionsectionrule, why not to roll your own socket server, app shapes that fit, Neblla à la carte, and what it connects to. Then `core` (the default):— the canonical six-file structure, the events-only client/server contract, and thefiveapppatterns.shapesThen(roomsfetchstart from one example; three room recipes in salas). Then the sections matching thefeaturesneblla.*youmodulesenabledthe app has: salas (rooms, lobby, matchmaking), servidor (the 3 server modes,setStatestate.set, anti-cheat), identidad (login/SSO/scopes/save), social ( ⟨39 unchanged words⟩ once). Skipping core risks producing incorrect Nebllaapps.apps; skipping empezar risks reaching for Neblla where a plain index.html was the better answer, or rebuilding by hand what it already gives you. Working on an existing app? Pass its `appId`: the guide comes cut to the app's neblla.* modules (E15) — only the chapters and sections of the modules it has, and `modules` says which. Each response lists the available `sections`. Most sections ⟨92 unchanged words⟩
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"appId":{"description":"An app's id: the guide cut to that app's modules (chapters and sections of modules it does not have are left out).","type":"string"},"lang":{"default":"es","description":"Language. The guide is written in Spanish and most chapters are also translated. \"en\" serves the English version of a section when one exists and falls back to the Spanish original otherwise, saying which in `langNote` — so it is always safe to ask for \"en\".","enum":["es","en"],"type":"string"},"maxLength":{"default":20000,"description":"Maximum ⟨54 unchanged words⟩ :"Which part of the guide to read.DefaultStart with \"empezar\" (whether Neblla fits and what shape to give the app), then \"core\" — the mandatorycontract.contract, and the default. \"index\" = table of contents, \"full\" = the whole guide.","enum":["index","full","empezar","core","salas","servidor","identidad ⟨11 unchanged words⟩
⟨5 unchanged words⟩ newest first, paginated) for moderation. Returns theplayers'users' messages in clear: only use it when ⟨7 unchanged words⟩
⟨10 unchanged words⟩ public ID","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"userId":{"description":"The user's mongo ObjectId ⟨14 unchanged words⟩
Keep an app files in sync with a GitHub repo.TheEachrepoenvironmentbecomesis a branch: `branch` is pre's, the source of truth:editingforthewhatappisfilesbeinginworkedNebllaon; `proBranch` (default "neblla-pro") isblockedpro's, and only neblla_promote writes it, by merging the commit pre was last synced from (made on the first promote; a push to it changes nothing for the users). One nuance worth getting right: while linked, every file tool (editneblla_upload_file,inneblla_edit_file,theneblla_delete_file, neblla_move_file) still works, and each call is a commit pushed to pre's branch of their repo with their token, then synced back, so an agent editing files lands commits on their branch, which is worth agreeing with them first. neblla_versions reads andpushrestores that repo's pre and pro branches. A sync also DELETES files that disappeared from the repo, so old hashed build chunks do not pile up. Neblla runs no build step: the build output must be committed, because a gitignored dist/ syncs nothing. Mind the per-app file cap too (30 on free, 150 on Pro). Actions: "repos" — the developer ⟨16 unchanged words⟩ "; "link" (url, optional branch, proBranch, subdir, syncModemanual|autoauto (default) | manual, token only if the developer has not ⟨18 unchanged words⟩ GitHub is linked; "sync" — pull now into pre (only changed files are downloaded); "set_mode" manual|auto; "test_branchcommit" (branch,filesorand/ornulldeletes,tooptionalremovemessage) —awriteprivatefilescopyback<appId>_testtowithTHEIRthereposamewithsettingsTHEIRthattoken,followsasanotheronebranchcommit,neverthen pull the result back: several files in one commit (thegallery;file tools make one commit each). "unlink".EachApullsynccostsdownloads only the files that changed, and its cost is not itemised in the quotacredits.table — so the case to watch is auto-sync on a large build, not the occasional manual pull.
⟨9 unchanged words⟩ branches","status","link","sync","set_modecommit","test_branchset_mode","unlink"],"type":"string"}, ⟨16 unchanged words⟩ ,"branch":{"description":"For link:livepre's branch, the one to follow (default: the repo default);","type":"string"},"deletes":{"description":"Forfor\"commit\":test_branchpaths to delete in the same commit","items":{"type":"string"},"type":"array"},"files":{"description":"For \"commit\": thetestfilesbranchto write,nullallremovesin one commit","items":{"additionalProperties":false,"properties":{"content":{"description":"The new contents, as text","type":"string"},"path":{"description":"Path as the app knows
List all apps owned by the authenticated developer. `shared`: the apps of other developers whose team they are on (neblla_team), with their paper (`access`: partner, support, viewer or screens) and what it lets them do on each area (`can`: read | write | null), and `invitations` waiting for them to accept (in the panel, «Compartido contigo»). On a shared app, use the tools its paper allows; the rest answer not_allowed.⟨18 unchanged words⟩ substring filter","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"fromFilter":{"default":"all","description": ⟨66 unchanged words⟩
⟨38 unchanged words⟩ tags, contentType, owner, status, moderation record, and stats: favorites, rating, reviews, uses, upvotes, downvotes, score); fetch a payload with neblla_get_community_entry. Paginate with ⟨5 unchanged words⟩
⟨21 unchanged words⟩ previous call","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":20,"maximum":50, ⟨4 unchanged words⟩ },"ownerId":{"description":"Filter by author. This is theauthorSAME per-app user idshownthatasneblla_list_users and neblla_search_users_by_email return (entry.owner.id is that id), so you can go straight from a user to everything they published — no name matching needed.","type":"string"},"q":{ ⟨7 unchanged words⟩ / tags","type":"string"},"sort":{"description":"Pending entries come most serious first by default (each carries `priority`: notice > serious > reports > prescreen > review); newest/oldest for plain date order; votes: most voted first (stats.score = votes for minus votes against)","enum":["gravity","newest","oldest","votes"],"type":"string"},"status":{"description":"Filter by status (omit for all): published, pending (awaits approval), rejected, removed, suspended (a formal notice is open), private (only its author sees it)","enum":["published","pending","rejected","removed","suspended","private"],"type":"string"}},"required" ⟨3 unchanged words⟩
Listallthe filesstaged forof anapp.app's pre (what is being edited; neblla_promote "diff" says how it differs from what the users have). Returns path, mimeType, size, and timestamps for each file.
⟨11 unchanged words⟩ rooms of any type and moderate the community. They are the members of the app's reserved group `moderators`, which lives in its data: a pre on pro's data has pro's, a pre with its own data its own (empty at first; env "pre" to see it).
⟨9 unchanged words⟩ app public ID","type":"string"},"env":{"description":"Whose moderators: \"pro\" (the users', default) or \"pre\" (its own unless it plays with pro's data)","enum":["pro","pre"],"type":"string"}},"required":["appId"],"type":"object"}
List the gallery reviews of an app (a review id — pass it as reviewId to neblla_reply_review, rating 1–5, text, language, author display name ⟨12 unchanged words⟩
⟨14 unchanged words⟩ , moderators, data, message count, last activity, their own inactivity days (`ttlInactivity`, null = the app's per-type value), how many players follow each one (`followers`: they get a notice when someone writes while they are away) and how many users are in each one right now.
⟨27 unchanged words⟩ is true.","type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":50,"maximum":100, ⟨39 unchanged words⟩
⟨11 unchanged words⟩ a pending entry, remove a published one, suspend a published one while a case is open, or restore arejected/removedrejected/suspended/removed one.`reject``suspend` hides the entry but is reversible and keeps everything; `remove` is the final one. `reject`, `remove` and `suspend` require a `reason` from the fixed taxonomy; the ⟨7 unchanged words⟩ live (communityUpdated) and in their own list. `reason` is one of: copyright, inappropriate, hate, harassment, spam, illegal, privacy, other. Note which queue you are working: the automatic action at the report threshold leaves the entry with status `pending` and `moderation.by = "reports"`, alongside entries merely awaiting first approval. `remove` keeps the entry and its payload so ⟨5 unchanged words⟩
⟨8 unchanged words⟩ ["approve","reject","remove","restore","suspend"],"type":"string"},"appId":{ ⟨44 unchanged words⟩
⟨9 unchanged words⟩ is the current path, newPath is the destination. In an app linked to its own repo, one commit on pre's branch.
⟨20 unchanged words⟩ restricted, with chargesEnabled / payoutsEnabled. "onboarding_link":atheone-timeneblla.comURLpagethewheredeveloperStripe's own onboarding openstoinside the developer's Neblla session, where they verify identity and add a bank account (a human must doitit; never ask for bank details or documents in the chat: Stripe collects them there, Neblla never sees them).; the first time it opens the account. A developer who already uses Stripe signs in with it there. Paid products waiting for the account publish by themselves once it is active. "dashboard_link":atheone-timedeveloper'sloginownURLStripetoDashboardthe(payments, payouts, refunds, disputes, taxes); they sign in with their StripeExpresslogin.dashboardThe(payoutsaccount is the developer's: Stripe's fees, refunds, disputes and any negative balance).are on it, not on Neblla. "disconnect" stops Neblla selling on their behalf and HIDES every paid product from users; the Stripe account itself is kept, so reconnecting later returns to the same one. It needs confirm: true, and only when the developer asked for it in as many words. Products stay hidden fromplayersusers until the status is active. Commission: Neblla takes 1 % of each sale, before tax; Stripe's own fee is separate and paid by the developer's account (the charge is made there: the developer is the seller).
⟨7 unchanged words⟩ "enum":["status","onboarding_link","dashboard_link","disconnect"],"type":"string"},"confirm":{"description":"Required true for \"disconnect\".","type":"boolean"}},"required":["action"],"type":"object"}
⟨130 unchanged words⟩ and description in at least the user's language.YouTheSHOULDappalsoistranslatelistedintoinotherthesupportedgallerylanguagesONLY for visitors whose language is one of those given (en, es, fr, de, it, pt, ja, ko, zh, ar)for—widerthereach,languagesbutof the Neblla website and gallery; they limit only theoriginallisting, never the app, which can use any language), and nothing isrequired.translated for you. Add other languages only if the developer wants visitors (and reviews) in them, and can read what those visitors write. Set defaultLang to the language the user originally ⟨18 unchanged words⟩
⟨108 unchanged words⟩ multiplayer\" (real-time/social), \"singleplayer\" (oneplayerperson at a time), or \"app\" ⟨51 unchanged words⟩
⟨16 unchanged words⟩ app user whose email is your developer email;log intoittheisappcreatedonceon first use if you never opened the app). Use it to seed a community with ⟨37 unchanged words⟩
⟨32 unchanged words⟩ "maxLength":64,"type":"string"},"i18n":{"additionalProperties":{"additionalProperties":false,"properties":{"summary":{"maxLength":500,"type":"string"},"title":{"maxLength":120,"type":"string"}},"type":"object"},"description":"Title/summary in other languages: { \"fr\": { \"title\": \"…\", \"summary\": \"…\" } } (up to 16). Search covers every language; readers asking for a locale get it","type":"object"},"lang":{"description":"Language of title/summary, e.g. \"es\"","maxLength":8,"type":"string"}," ⟨49 unchanged words⟩
⟨25 unchanged words⟩ "minLength":2,"type":"string"},"env":{"default":"pro","description":"Whose data: \"pro\" (the users', default) or \"pre\" (the app's pre, its own users and data unless it plays with pro's)","enum":["pro","pre"],"type":"string"},"limit":{"default":50,"maximum":50, ⟨16 unchanged words⟩
Set aplayer'suser's ban state in this app: "none" ⟨2 unchanged words⟩ the app shows the ban message and blocksplayplay; Neblla refuses everything but their messages to you), "terminated" (wipesblocksthethemplayer'sforlocalgood:datatheirindevice is sealed so thebrowserappandnodisconnectslongerthemopens—there,irreversibleandfortheythataredatadisconnected), "community_muted" (can play, and can still REPORT entries, but cannot publish, rate orreportreview).inTakes effect live. WARNING: "terminated" seals thecommunityuser's device, and lifting it with "none" does not unseal a device that is already sealed (the user has to clear the site's data) — never pass it unless the developer asked for that specific value. The other two are fully reversible with "none".TakesThiseffecttoollive.keeps no reason, no note and no history: if the developer needs a record of why someone was sanctioned, write it yourself with neblla_send_communication or in a community case note. Confirm with the developer before banned/terminated.
⟨25 unchanged words⟩ string"},"userId":{"description":"Theplayer'suser's `_id` from neblla_list_users","type":"string"} ⟨6 unchanged words⟩
Make aplayeruser an app moderator (`moderator: true`) or remove them (`false`).: it adds them to, or takes them out of, the app's reserved group `moderators`. `userId` is the per-app user id (from neblla_list_users with the same env), not an email. The group lives in the app's data: with env "pre" and a pre with its own data, it names a moderator of pre (the developer adds themselves to moderate there); a pre on pro's data shares pro's.
⟨10 unchanged words⟩ public ID","type":"string"},"env":{"description":"Whose moderators: \"pro\" (the users', default) or \"pre\" (its own unless it plays with pro's data)","enum":["pro","pre"],"type":"string"},"moderator":{"description":"true = add, ⟨18 unchanged words⟩
⟨5 unchanged words⟩ app's public slug (the readable name inplay.neblla.com/<slug>play.neblla.com/<slug or id>). `checkOnly: true` only tells whether it is valid and free. `slug: null` removesitit.(An old sluglinkskeepsstopleadingworkingto the app (it stays the app's; nobody else can take it)., and the users' sessions follow. At least 5 characters. Changing it changes the public URL — confirm with the developer.
⟨16 unchanged words⟩ the allowed-origins list atomically without overwriting other entries.`sandbox`compiled: true` lets a compiled app (Capacitor, Ionic, Tauri, Electron) use the app:loadit takes their origins (capacitor://localhost, https://localhost, null…) without a domain, and its new guests are limited per address. A page on localhost/opensnon-httpsonlyoriginsthe app's PRE, with its local key (neblla_pre_access). `turnstile`: Cloudflare Turnstile before a new guest is made (stops scripts creating guests in bulk; returning users are not asked): true turns it on: with the developer's own widget if one is saved, else Neblla's, which works on <app>.neblla.app and play.neblla.com but not on a domain of the developer's (turnstile_own_widget_required; turnstile_unavailable if this server has none); { siteKey, secretKey } uses the developer's own widget, fordevelopment.a domain of theirs (secretKey is never read back; leave it out to keep it); false turns it off and keeps their keys; null turns it off and forgets them. `ai` is the AI proxy config for the SDK'sneblla.aineblla.ai.prompt() — set { model, maxOutputTokens, userTokenMode ⟨35 unchanged words⟩ } map of named prompt prefixes invoked vianeblla.aineblla.ai.prompt(text, { preset: 'name' }). limitsEnabled + tokensPerDay cap how many Claude tokens each end-user can spend perday;day (on by default, 10,000); only applied when userTokenMode = off. With userTokenMode off, model is a ceiling: a user may ask for it or a cheaper one of the same provider (Claude: haiku < sonnet < opus; OpenAI: gpt-5-nano < gpt-5-mini < gpt-5; DeepSeek: deepseek-chat < deepseek-reasoner), a dearer or unlisted one is refused (model_not_allowed). In browser/server a call without the user's key is refused (missing_user_token), never billed to the developer. `inbox`: true/false, or { maxPerUser,maxOutboundPerUser} totunelower thein-appper-usercommunicationscapinboxwithin the plan ceiling (null100=messagesdefaultson free, 2,000 on Pro); the inbox itself is the inbox module: features: { inbox: true }. `serverMode`: "js" (Fog) | "wasmown" | "infracloud" (infraServer).needs`fog`:an{activewindowPro(ms,subscriptiondefault 1000), latencyWatch }. `rooms`: the rooms policy object ({ policy, allowedTypes, defaultType, public, private, requireLogin, … }, as returned in the app); `requireLogin: true` keeps guests out of every room but the lobby (they get login_required). A rooms change that discards saved rooms ⟨16 unchanged words⟩ agree, repeat with `confirmRoomsChange: true`. `p2p` (P2P/TURN{settingsallow,e.g. {turnmedia}:trueallow=false}turns WebRTC off; turn=true offers Neblla's TURN relay, which spends quota), `connections` ("all"|"p2p"|"server"|"none"), `theme` (palette/typography/layout), `permissionJustifications` ({microphone:email,"why"friends,…discoverable }): one sentence per SSO scope,`guestTtl`shown(daystoatheguestUSERaccountonlivesthe consent screen under the built-in description of that scope — not browser permissions), `pattern` (app pattern id).Writing`savedReplies`: the inbox's saved replies, answers the developer picks when answering a user in the panel (Gente › Buzón): [{ id?, title, text }], at most 50, title up toan80appcharacters,thattexthasupantounapplied5000;draftreplacesdiscardsthethatwholedraftlist (read it in neblla_get_app, app.savedReplies; keep each id to keep that reply), null clears it. The users never see theanswerlist.saysEvery`draftDiscardedsetting here except name, compiled, turnstile, moderationLog, playerStats, keepFogCases, savedReplies, addDomain/removeDomain, erasureWindow, playerQuota and customDomain lands in the app's PRE:true`).the developer sees it at the private link, and the users get it with neblla_promote. `community` configures user-published entries (neblla.community, see the ⟨13 unchanged words⟩ , maxPayloadBytes, categories[], contentTypes[], maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: "off"|"flag"|"hold", images }, downvotes (false by default: users vote entries only for; true lets them vote against too) }. prescreen is an AI check when a user publishes or edits, run with the developer’s own Claude key (it costs their tokens; off by default): "flag" publishes and labels what the model objects to (entry.flagged), "hold" sends it to the moderation queue. The communityfeaturemodule itself isenabledswitched on with `features: { community: true }` (it needs login).`.
before
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI proxy config: { model, maxOutputTokens, userTokenMode, presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser, rules }. Merged over the stored config.","properties":{},"type":"object"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connections":{"description":"Allowed connection kinds","enum":["all","p2p","server","none"],"type":"string"},"features":{"additionalProperties":true,"description":"Feature switches to merge into the app: { multiplayer, communications, purchases, ai, friends, login, anticheat, community } as booleans. Only the keys you pass change.","properties":{},"type":"object"},"guestTtl":{"description":"Days a guest (no-login) account is kept","minimum":0,"type":"integer"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, maxOutboundPerUser }; null = defaults"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart }. In \"nocode\" mode this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file."},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"P2P / TURN settings, e.g. { media: true }"},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app needs each browser permission: { microphone, camera, geolocation, midi }","properties":{},"type":"object"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"sandbox":{"description":"Allow non-https / localhost loads for development","type":"boolean"},"serverMode":{"description":"Where server/*.js runs: js (a player device), wasm, infra (Neblla servers, Pro only)","enum":["js","wasm","infra"],"type":"string"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"}},"required":["appId"],"type":"object"}⟨6 unchanged words⟩ {"currency":{"description":"ISO 4217 code Stripe charges in, e.g. \"usd\"; the price is checked again in it","pattern":"^[a-zA-Z]{3}$","type":"string"},"defaultLang":{"enum" ⟨30 unchanged words⟩ image URL","type":"string"},"interval":{"description":"subscription: how often it renews (new subscribers)","enum":["month","year"],"type":"string"},"max":{"description":"Maximum quantity for consumable ⟨30 unchanged words⟩ price":{"description":"Price in cents (one_timeone_time; per period for subscription — applies to new subscribers)","type":"number"},"pricePerUnit": ⟨20 unchanged words⟩ :{"description":"Whether the product is buyable (without an active payout account it waits, as in neblla_create_product)","type":"boolean"},"step":{"description":"Quantity step for consumable selector","type":"number"},"trialDays":{"description":"subscription: free trial days (new subscribers)","maximum":730,"minimum":0,"type":"integer"}},"required":["productId"],"type":"object"}
Change a room: replace its shared `data` (playersusers in the room receive it live), its `moderators` list and/or its`moderators`ownlist.inactivity time (`ttlInactivity` in days, 0 = never deleted, null = back to the app's per-type value). Replacing data overwrites the live game state of ⟨8 unchanged words⟩
⟨43 unchanged words⟩ ":"Room name","type":"string"},"ttlInactivity":{"anyOf":[{"description":"Days without activity before Neblla deletes this room (0 = never). Overrides the app's per-type value (rooms.public/private.ttlInactivity: 30 / 90 by default) for this room only.","maximum":3650,"minimum":0,"type":"integer"},{"type":"null"}],"description":"Days without activity before Neblla deletes this room (0 = never). Overrides the app's per-type value (rooms.public/private.ttlInactivity: 30 / 90 by default) for this room only."}},"required":["appId","name"],"type":"object"}
ReplaceREPLACES (does not merge) the `data` object for a specific end user (the same object the SDK'sneblla.saveneblla.db.save()reads/writeswrites and neblla.db.data shows). There is no merge option here, unlike the SDK which offers shallow/deep/crdt: whatever you pass becomes the whole document, so read it first with neblla_get_user, modify, and write it back — and be aware the user may be saving at the same moment. Use sparingly: this overwrites the user's saved state and the change is broadcast live. Useful for support (unstuck aplayeruser), moderation (clear a corrupted save), ⟨38 unchanged words⟩
Upload or update a file for an app. Mind the per-app file cap before a bulk import: 30 files on free, 150 on Pro, 25 MB per file. Pass text files (HTML, CSS, JS, ⟨87 unchanged words⟩ the same path again overwrites the previous version. Files land in the app's PRE (what the developer tries at the private link); the users get them with neblla_promote. An app linked to its own repo (neblla_git) takes each upload as a commit on pre's branch of that repo, then syncs it back. Canonical paths for a Neblla app are index.html ⟨69 unchanged words⟩ Communication between client.js and the server files isevents-onlymessages and state only (sendMessagemessage.send /tellPresentermessage.to('server'/|tellServer'presenter'/|messageToServerid, data) /messageFromServermessage.on(m => …) /setStatestate.set /on('message')state.on) — never direct function calls. All three server/ files run on the room's SERVER PEER(— a device the SDKpickselects by latency and CPU,neverwhich EXCLUDES only boards, so in peer mode it may well be the presenter. The presenter)is a different role: the first device into the room; if it leaves, the SDK promotes another device (chosen like the server peer, not by arrival order) and fires neblla.room.on('presenter'). server/lobby.js for the lobby (matchmaking); server/gathering.js while a non-lobby room is in 'gathering' state (decide when to callneblla.startRoomneblla.room.start() to start the game); server/room.jspost-startRoomafter room.start (authoritative game logic —handlehearmessageToServerplayers with neblla.message.on(m => { if (m.to !== 'server') return; … }), broadcast withsendMessageneblla.message.send /setStateneblla.state.set).
⟨113 unchanged words⟩ ,"base64"],"type":"string"},"message":{"description":"A short note about this change. It names this version in the app history (neblla_versions), so the history reads like a changelog instead of \"upload: 3 file(s)\". A burst of uploads is one version: pass the same note on each.","maxLength":120,"type":"string"},"path":{"description":"File path within the ⟨4 unchanged words⟩ , \"main.css\", \"client.js\", \"server.js\server/room.js\". Subfolders allowed for assets.","type": ⟨6 unchanged words⟩
⟨182 unchanged words⟩ with no separate rooms; multi = separate rooms),structureborn with one room example: the server sets the state, the clients paint it and talk through message; a referee, a big screen with phones as pads or everyone drawing at once are recipes in neblla_get_llms(turn-based|host-controllers|collab-realtime;{multisection:only"salas" })),login/friends/purchases/ai/communicationsboard and master (booleansmulti—only: a shared display screen, someone who leads thefeaturesession;pieceseach adds its example piece),connectionsmodules (all|p2p|serverthe list of neblla.* modules the app carries beyond auth and db, always on, and the multiplayer ones players sets: any of social, shop, inbox, ai, api, anticheat, groups, community, business, tools, or [] — each option says what it does, what it needs and what it costs; decide it WITH the developer, telling them the cost and duty of what you pick),serverModeconnections (js|wasmall|p2p|server), roomsPolicy (ephemeral|public|private|mixed), matchmakingMode (nocode|custom), aiPayer (off|browser|server; when ai isonin modules), palette ({primary, secondary, alert} ⟨19 unchanged words⟩
⟨20 unchanged words⟩ \"game\", players: \"multi\",structuremodules: [\"turn-based\social\",login: true\"anticheat\"], palette: { primary: \"#0F7BD7\", ⟨23 unchanged words⟩
The app's alerts, as its panel shows them: what needs the developer now (store full with its 7-day countdown, their own database not answering, payments waiting for it, a data migration paused for the quota, quota out or in its 10 % margin, users saving in a loop, formal legal notices on community entries, the community put on hold by Neblla, data erasure requests, reviews without an answer, a "Pasar a pro" batch that failed, automatic recharges done or failed) and the history (90 days). Each alert says in plain words what happens (`text.what`), what the users notice (`text.users`) and what t…
The app's secret login key, for apps that already have their own users ("Tu propio login" in neblla_get_llms({ section: "identidad" })). Their server calls POST https://neblla.com/api/apps/<appId>/auth/token with Authorization: Bearer <key> and { externalId, email?, emailVerified?, name? }, gets a one-use token valid one minute, and the front calls neblla.auth.login({ token }). action "create" makes the key (refused if one exists), "rotate" replaces it (the old one stops working at once), "status" says whether one exists, "revoke" removes it. The key is returned ONLY by create and rotate: tel…
The backups of the data Neblla keeps for an app: the copies of the last 7 days (30 with Pro; `keepDays` says which), taken from Neblla's daily encrypted copy of its databases, and restoring one whole. Actions: "list" (enabled: whether Neblla keeps copies at all; envs.pro / envs.pre: byodb, usesProData, the last restore; days: [{ date, createdAt, pro, pre }] with how many rows of each kind the copy holds — players, saves, tables, inbox, community, chat, rooms, purchases, customers; null with the developer's own database, which Neblla does not copy); "restore" (date, env "pro" by default or "pr…
What the developer pays Neblla for one app: the Pro subscription (€10 a month per app, or $10 outside Europe, taxes included) and one-off quota packs. "pro_checkout" and "quota_pack_checkout" return a URL the DEVELOPER opens to pay — an agent cannot complete a payment, and nothing is granted until Stripe confirms it, so tell them the upgrade lands a few seconds after they finish. "cancel_pro" stops Pro from renewing: it stays until the end of the period already paid (answers endsAt) and then the app is Free; nothing is refunded. Only call it when the developer has asked for it in as many word…
Bookings (business template), the administration side: action "createSlot" { set, start, end?, capacity, title?, repeat? } — repeat is a calendar rule (the usual RFC 5545 RRULE): { freq: "daily"|"weekly"|"monthly"|"yearly", interval?, byDay? (["MO","WE"]; monthly "2TU" = second Tuesday, "-1FR" = last Friday), byMonthDay? (15, -1 = last day), byMonth?, until? (a date: that whole day) or count?, except? (dates left out), timeZone? (IANA: keeps the local hour when the clocks change) } or an RRULE string ("FREQ=WEEKLY;BYDAY=TU,TH;COUNT=20"); its cases are slots sharing a series id, made a year ah…
Write to ALL the users of an app, or to the members of one group (optionally only one role in it): the message lands in each one's inbox as a message from the developer (neblla.inbox; a connected user gets it live, inbox.on('updated') with action "created"). It is paid per recipient from the app's daily quota, like any message (msgFixedOut + the bytes of message and data, kind newCommunication). action "estimate" { message, data?, group?, role? } says beforehand what it will cost: recipients, credits, what is left today (available), percentOfDay and how many that reaches (reachable) — ALWAYS …
The developer’s server on THEIR Cloudflare account, deployed by Neblla (serverMode "own"; Cloudflare bills them, Neblla only the relay). action "set" { provider: "cloudflare", accountId, apiToken }: checks the token (it needs Workers Scripts: Edit, and the account’s workers.dev subdomain on), keeps it sealed, switches the app to "own" and deploys the box — the same room.js runner as Neblla’s cloud; saving a server/ file redeploys it. "status": the account, the box and its last deploy (never the token). "remove": takes the Worker off their account and forgets the token (the app stays "own": `n…
Formal notices filed about the community entries of this app from OUTSIDE it, with no account — the route a rights holder or anyone who spots illegal content uses. Filing one suspends the entry immediately and opens a case here. Actions: "list" (optional status open|countered|upheld|dismissed) — open cases first, since those are the ones needing a human; "get" (caseId) — the full claim; "close" (caseId, outcome upheld|dismissed) — decides the CASE only. Closing does NOT put the entry back: restoring is a separate, deliberate act (neblla_moderate_community_entry with "restore"), so that dismis…
Make an incoming webhook: a URL a third party (Twitch, a payment provider, n8n…) calls with a JSON body, and Neblla hands that body to a room — its server file hears neblla.room.on("hook", ({ name, data }) => …) on the Fogs — or, with to: "user", to the user named in the call (?user=<their id in the app>). The URL carries a token shown only now; making it again with the same name gives a new token (the old URL stops working). Up to 20 per app; 64 KB per call; 60 calls a minute per hook. Answers: 200 { delivered }, 404 hook_not_found, 409 room_not_live / user_not_online. No server of yours nee…
Credits (business template), the administration side: action "balance" { owner, kind? } or "adjust" { owner, kind?, delta } (a refund by hand, a gift; never below 0). Credit kinds and the products that top them up: neblla_update_app({ business: { credits: { <kind>: { products: { <productId>: n } } } } }).
Your word on a Fog case. "flag_others": the flag moves from the odd one to the Fogs of the majority (a coordinated attack — rare). "annul": the flag goes and Neblla reviews the case, states included (you saw a bug where the anticheat saw a cheat). Leaving a case alone keeps it as it is. One decision per case.
Delete an API secret; the app's calls to it answer secret_not_found from then on.
Delete an automation.
Withdraw an entry of your app's community for good: the entry, its payload, its reviews and favorites are deleted and its bytes freed (entries hanging from it stay, without their parent). This is not moderation — no reason, no record, nothing counts against the author. To take down a user's entry for breaking the rules, use neblla_moderate_community_entry (remove) instead.
Delete an incoming webhook; its URL answers hook_not_found from then on.
The developer's Neblla inbox: notices from Neblla to every developer, such as a change in the list of sub-processors announced 30 days ahead (the DPA). action "list" (default) shows the ones not dismissed; "dismiss" with an id hides one. Tell the developer what they say.
Edit a text file of an app by pieces, without sending the whole file: a list of `edits`, each an `oldText` to find and the `newText` to put in its place (like your own editor). Each oldText must appear exactly once in the file (include a few lines around it to make it unique), or pass `replaceAll: true` to change every place it appears. The edits apply in order, each on the result of the ones before; if any fails, none is applied and the answer says which one and why. Pass `baseVersion` (the `version` neblla_get_file returned) so the edit fails with version_changed instead of overwriting a ch…
Erase ONE user of this app — for a data-deletion (RGPD) request the user made to the developer. Deletes their saved data and messages in the app store and their record in this app; their review and favourite go too. Their Neblla account and their other apps are NOT touched (that is for the user to delete), and purchases are kept as payment records. Irreversible: pass confirm: true, and only when the developer asked for this user to be erased.
Export the users of an app WITH everything the app holds about them: the full record (saved data, public info, communications, stats, language, the email if it was shared) and their purchase balances. For a developer taking their app data with them. Paginated by cursor: pass the `next` you got back as `after` until it is null. The cross-app Neblla account id is not included — only the per-app `_id`.
Add a flag to a user of this app by hand, as the anti-cheat does when it catches something (users.flags, the count neblla_list_users shows), or take one back with delta -1 (never below 0). A flag is a mark for the developer to look at: it never bans, mutes or warns the user, and the user is not told. An added flag reaches the developer's `flag` webhook with by: "developer". Returns the user's flags now.
The app's activity log, the last 90 days, newest first: who changed what and when — the developer from the panel, their team, and you through this MCP (via "mcp", which the panel shows as "from Claude"): settings, publishing, files, the team and who opens pre, products, the app's people, moderation, the back office screens. Lines { id, at, by (an email), who: owner | team | moderator, access? (the paper of a team member: partner | support | viewer | screens), via: panel | mcp | app, area, action ("app.update", "file.upload", "user.ban"…), detail?, values?, target?, count? }: detail names what…
The app's log while it runs, the last 7 days (30 with Pro; `keepDays` says which), the latest first, per environment (env "pro", the published app, by default; "pre", the draft): what to read first when something fails. Kinds: "error" — what the app's own code threw and nobody caught: on a page (source "client": client.js or another of its files; an uncaught promise too) or in a server file's handler on the room's server peer (source "server": lobby.js, gathering.js, room.js), with `file`, `line`, `col`, `message`, `name`, `stack` and the `room`; the SDK sends at most 10 a minute from each pa…
Read the app's moderation log, newest first: who named or removed a moderator (of the app or of a room), each ban, every room message, inbox message or community entry someone other than its author took down. Each line: { kind, by ("dev:<email>" or a moderator's per-app user id), target, room?, detail?, at }. `on` says whether the app keeps the log; it is off by default (neblla_update_app moderationLog: true).
The app's statistics (the panel's Statistics tab), kept only while `playerStats` is on (off by default; turn it on with neblla_update_app playerStats: true, in the act — confirm with the developer, it costs quota). The last `days` UTC days (30 by default), today included, of signed-in players: `cards` { activeToday, activeYesterday, activeThisWeek, activeLastWeek, weekTrend (% vs the week before), dailyActiveAvg7, dailyActiveTrend, newPlayers, newTrend (% vs the period before), visits, avgMinutesPerDay (per active player and day, with the app open and in sight), avgMinutesPerVisit, returns { …
Groups of users with a role each (a team, a client), for rows that belong to a group: neblla.db.save(data, { table, group }). Roles and their rights per table are declared with neblla_update_app({ roles }). Actions: "create" { name }, "delete" { name } (with its members; its rows stay), "add" { group, user, role } (user: the user id in the app, as neblla_list_users gives it; adding again changes the role), "remove" { group, user }, "list" (the groups, or { group } its members), "of" { user } (their groups). An employee and a client are different roles in the same group. Also from the admin AP…
The app's API secrets: name, baseUrl, headers, query, limits and fromClient. Never the values.
The app’s automations with their schedule, action, next run and the result of the last one.
The users who asked, from inside your app (neblla.auth.requestErasure), that you erase the data they gave it. Erase one with neblla_erase_user (its userObjectId); that closes the request. Pending ones by default.
The app’s Fog cases, newest first: when the Fogs running a room’s server file computed different states, Neblla compared them, the majority won, and the odd one’s user was flagged. Each case: room, seq, status (flagged | undecided | reassigned | annulled), fogs (userId, appUserId, leader, verdict: majority | odd | missing | undecided), diff (only the paths that differ), flagged. Undecided: no majority (two Fogs, or three different states) — nobody was flagged.
The app’s incoming webhooks: name, to, room. Never their tokens (make one again for a new URL).
The app’s last outgoing webhook deliveries (neblla_update_app({ webhooks })), newest first: event, secret, path, at, ok, and the endpoint’s status or the error.
Who else may open the app's PRE (its private test version at play.neblla.com/pre/<slug or id>/). The developer always can. Two ways in: Neblla accounts by email (they sign in on the pre page), and links with a password for people without an account. Either can have an expiry. Once in, a person keeps a pass for 4 hours even if they sign out, so they can play as a guest; removing someone stops new passes, not one already given. Actions: "list"; "add" (email, expiresAt?) — let an account in, or change its expiry; "remove" (email); "link" (password?, expiresAt?, label?) — a new link; without a pa…
Which data the app's PRE (its private test version) plays with. "own" (the default): its own users, saves, tables, inbox and community, empty at first and apart from the users'. "pro": the users' real data, read and written for real. Rooms are always pre's own, so testers never share a live room with users. Changing it signs out whoever is signed in to pre. Without "use", returns the current one. Switching to "pro" lets pre's unreleased code write the users' data: only when the developer asked for it.
Whether the app's PRE sells through Stripe's TEST mode: on by default, so testers buy with Stripe's test cards and nothing is charged. Pre always sells the app's own products (one catalogue); in test mode it also lists the ones not published yet. Test purchases are marked as such: pro never counts them, not even when pre plays with pro's data (neblla_pre_data), and there they give no credits. Without "on", returns the current value. Also says "testMode" ("separate": the developer needs a test account of their own, opened at onboardingUrl with Stripe's test data; "same": this server is in test…
Every app has two environments. PRE is where every change from this MCP and the dashboard lands (files, modules, rooms, theme, AI…); the developer plays it at a private link. PRO is what the users have: it never changes until this tool sends pre to it. Actions: "diff" — what would change (files added/changed/removed, settings that differ; differs:false means pro already matches; `promotedAt` is when the users last got a version; `queued` is null, except for a send made before publishing became instant that has not landed yet: { at, files, nextBatchAt, capped, failed, deployed }, which Neblla …
The developer’s app as its OWN app in the stores, with THEIR developer account (Android now; iOS after). store "google": action "set" { packageName (e.g. com.studio.game; fixed once published), serviceAccountJson (a Play service account with release rights, kept sealed), track? (internal|alpha|beta|production), certs? (the app signing SHA-256 from Play Console → App integrity: the app then opens full screen) }; "build" builds the Android bundle from the app’s page (play.neblla.com/<slug or id> or its own domain) and uploads it to that track — it runs in the background: call "status" for the r…
Answer a review of an entry in your app's community, as the app: the answer shows under the review for everyone. One answer per review; sending another replaces it, and text null takes it back. reviewerId is the review's author.id (neblla_get_community_entry lists the latest reviews). The entry's author can answer from the SDK too (community.reply).
Change the palette and/or typography of an app created with the wizard or neblla_create_app, AFTER creation. Re-bakes only the theme tokens (--nb-bg, --nb-primary, …, --nb-font-title, --nb-font-body) in main.css and the Google Fonts link in index.html; every other line stays as the developer left it. Tokens the developer deleted are not put back: they come back in `missing`. Stores the theme on the app. (neblla_update_app with `theme` only stores it, without touching the files.)
Run an automation now, to try it (its schedule does not change).
See a panel as the back office shows it: { appId, name, inputs? } → { title, columns, filters, actions, rows }.
The app's sales (Dinero › Ventas in the dashboard): action "list" { status?, productId?, from?, to?, skip?, limit? } — newest first, each with product, units, paid (minor units) and currency when known, status (completed | refunded | charged_back | ended), refund and dispute, the buyer's userId, email and buyerName, canRefund. "documents" { saleId } — the links Stripe keeps for the buyer: a payment's receipt (receiptUrl) or a subscription's invoices (url, pdf); nothing is stored. "refund" { saleId } — gives the WHOLE payment back in Stripe (Neblla's 1 % with it) and takes the purchase back at…
The rankings of a game for one (neblla.scores; boards declared with neblla_update_app({ scores })). action "top": the best of a board, { board, period: "all" (default) | "day" (today, UTC), limit (≤ 100) } → [{ place, player, value, at, info }]; action "player": one user's standing { board, player } → { best, bestAt, dayBest, runs, place } or null; action "remove": take a user's score off a board { board, player } (a cheat that got in). Refused scores never enter: they are in the app log (neblla_get_logs, area score).
The app's server key, for running room.js on the developer's own server (serverMode "own", see neblla_get_llms({ section: "servidor" }) "Server"). Their server runs `npx @neblla/server --app <appId> --key $NEBLLA_SERVER_KEY ./server`: it calls Neblla, no open ports or public URL. action "create" makes the key (refused if one exists), "rotate" replaces it (the old one stops at once), "status" says whether one exists, "revoke" removes it. The key is returned ONLY by create and rotate: it goes in the server's environment, never in the app.
Create or change an API secret: an external API the app reaches with neblla.api.call(name, path, body) without a server of yours. Neblla fills the secret in where {secret} appears in `headers` or `query`, and calls only under `baseUrl` (https, a public host). The value is written and never read back (neblla_list_api_secrets shows everything else); on a change, leave `value` out to keep it. From the server file (room.js…) it always works; from a user's client only with fromClient: true. Limits: loggedInOnly, calls per user per minute / hour / sixHours / day / week / month, and a whole-app cap …
Create or replace an automation: a scheduled action on the app (open a room, hand a room data, call one of your API secrets). For flows across the tools the developer already uses, n8n, Make or Zapier connect through the admin API, incoming webhooks and outgoing webhooks. when: { at: ISO date } once; { every: minutes } (5 – 10080); or { daily: "HH:MM", days?: [0–6, 0 = Sunday], tz?: "Europe/Madrid" (UTC by default) }. action: { kind: "openRoom", room, config?, openForMinutes? (60) } books the room with its config, so whoever enters first finds it in neblla.room.config; { kind: "message", room…
Create, replace or (panel: null) remove a back office panel: a screen for the developer and their team. panel: { title, source: { table } | { booking: set } | { credits: kind }, columns: ["field" | { field, label }] (player.name, start, title… too), where: { field: value | { gte: "$today", lt: "$tomorrow" } } (dates: $now, $today, $tomorrow, $weekStart, $monthStart, $nextMonthStart), filters: [{ field, op?, label?, type? }] the person fills in, orderBy, limit (≤ 500), actions: [{ label, call: "booking.cancel" | "booking.move" | "credits.adjust" | "table.remove" | "table.save", args: { … "$row…
Read an app table across users: filters on declared fields, order, page, or just a count. e.g. { table: "reservas", where: { clase: "yoga-martes", estado: "confirmada" }, count: true }, or { table: "puntos", orderBy: "-score", limit: 10 }. Rows come with id, owner (the user id in the app), createdAt, updatedAt.
Write or remove one row of an app table by hand (support, a fix): action "save" with { owner, data } for a new row of that user, or { id, data } to replace an existing row; action "remove" with { id }. Users never see other users’ rows; this is the developer side.
Taxes on the developer’s sales to users, with Stripe Tax on their own payout account (they are the seller; the taxes are theirs, Neblla only sets it up for them). "status": settings status (active/pending, and what is missing), head office, defaults and active registrations. "set_head_office": the business address ({ country, postal_code, city, line1, state? }). "set_defaults": the default product tax code (txcd_…, e.g. txcd_10000000 for general digital goods — check with the developer) and tax behaviour. "add_registration" / "expire_registration": the jurisdictions where the developer is reg…
The developer’s team for an app (Gente › Tu equipo in the panel): people who sign in to developers.neblla.com with their own email, and use this MCP with their own account, each with a paper (`access`) that the server applies on every route of the panel and every tool here: "partner" (Socio: everything but money, the plan and deleting the app), "support" (Comunidad y soporte: the people of the app — users, inbox, bookings, community, groups, moderation — and the sales, read only), "viewer" (Solo mirar: everything, read only) or "screens" (Solo una pantalla, the default: only the custom screen…
Trades between the app's users (C170: neblla.db.offer / acceptOffer; what is tradable is neblla_update_app({ trade })). Action "list": the log, newest first, with { user? (their id in the app), status? (pending | done | declined | cancelled | expired | failed | all, the default), limit?, offset? } → { trades, total }; each trade has from, to, give, get (items { table, id } or { product, amount }), status, reason (why it failed or was cancelled), createdAt, expiresAt, closedAt. Action "cancel" { id }: take a pending offer back (support, abuse); both users hear it. A trade that was done is not …
Edit any entry of your app's community — yours or a user's — as the developer: title, summary, category, tags, lang, i18n (replaces the whole map; null clears it) and/or payload (replaced whole). Only the fields you pass change. Editing a user's entry records that you did and tells them. Use it to keep a community fed from elsewhere in sync.
The version history of an app, with a branch per environment: "pre" (every save burst is a commit) and "pro" (each neblla_promote is a merge of pre into it), so this is how an app is rolled back. Without its own repo, Neblla keeps that git for it; linked to its own GitHub repo (neblla_git), the history is that repo's pre and pro branches, read and written with the developer's token, and a restore is a new commit on pre's branch. Actions: "history" (branch?) — the versions, newest first; "files" (commit, branch?) — what one version holds; "restore" (commit, branch?) — put the app's pre back to…
Drafts: a private copy of an app (id "_<appId>") to change files and settings without touching the live app. "info" says whether one exists; "create" makes it (edit it by passing its id to the file/app tools); "apply" promotes the draft over the live app (replaces its files and settings — confirm first; a rooms change that loses data needs confirmRoomsChange); "discard" deletes the draft.
Local backup of an app's files before risky edits. action "create" copies every file to <path>_copy (replacing any previous snapshot); "rollback" restores the copies over the current files (overwrites them — confirm first); "discard" deletes the copies. Use create before a large refactor, rollback if it goes wrong.
⟨110 unchanged words⟩ per day; only applied when userTokenMode = off.`inbox`inbox`:true`true/false,enablesor { maxPerUser, maxOutboundPerUser } to tune the in-app communications inboxfeature(nullfor=enddefaults).users.`serverMode`: "js" | "wasm" | "infra" (infra needs an active Pro subscription). `rooms`: the rooms policy object ({ policy, allowedTypes, defaultType, public, private, … }, as returned in the app). A rooms change that discards saved rooms data answers error "rooms_data_loss" with the warnings — show them to the developer and, if they agree, repeat with `confirmRoomsChange: true`. `p2p` (P2P/TURN settings, e.g. { media: true }), `connections` ("all"|"p2p"|"server"|"none"), `theme` (palette/typography/layout), `permissionJustifications` ({ microphone: "why", … }), `guestTtl` (days a guest account lives), `pattern` (app pattern id). Writing to an app that has an unapplied draft discards that draft (the answer says `draftDiscarded: true`). `community` configures user-published entries (neblla.community, see the ⟨34 unchanged words⟩
⟨74 unchanged words⟩ properties":{},"type":"object"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connections":{"description":"Allowed connection kinds","enum":["all","p2p","server","none"],"type":"string"},"features":{"additionalProperties":true,"description":"Feature ⟨22 unchanged words⟩ properties":{},"type":"object"},"inboxguestTtl":{"description":"Enable/disableDaysin-appacommunicationsguestinbox(no-login)foraccountendisuserskept","minimum":0,"type":"integer"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, maxOutboundPerUser }; null = defaults"},"matchmaking":{"anyOf":[{"additionalProperties":true, ⟨50 unchanged words⟩ display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"P2P / TURN settings, e.g. { media: true }"},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app needs each browser permission: { microphone, camera, geolocation, midi }","properties":{},"type":"object"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"sandbox":{"description":"Allow non-https / localhost loads for development","type":"boolean"},"serverMode":{"description":"Where server/*.js runs: js (a player device), wasm, infra (Neblla servers, Pro only)","enum":["js","wasm","infra"],"type":"string"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"}},"required":["appId"],"type":"object"}
Archive an app nobody uses: its data is bundled onto the CDN and cleared from Neblla's database. It stays in the gallery and is restored automatically the next time anyone opens it (first open is slower). Neblla also does this on its own for long-unused apps.
Read the message banned players see in this app, or change it by passing `defaultMessage`.
Bring-your-own-database: move where this app stores its players' data (saves, rooms, community) from Neblla's default store to the developer's own database. Actions: "status" (current engine + migration progress); "validate" (engine + config, tests the connection, changes nothing); "migrate" (validates again, then starts a 3-phase background migration of ALL player data — confirm with the developer first); "cancel" (only before the last phase). Engines and config: mongo {uri, dbName}, turso {url, authToken}, postgres {uri}, mysql {uri}. Credentials are stored encrypted and never returned.
Consume units of a player's purchased product from the backoffice (e.g. refund-by-hand, support fixes). Atomic: fails with insufficient_amount if the player has fewer units. The player's game gets a live `purchase` event.
Copy the behavioural settings (features, rooms policy, matchmaking, inbox, p2p, serverMode, permissions, theme, connections, ai…) from one of your apps onto another. Name, files, domains, slug, gallery and billing are never copied. Overwrites those settings on the target — confirm first.
Create a predefined room (it exists before anyone joins). Name: A-Z 0-9 _ - (stored uppercase); LOBBY is reserved. `permanent: true` keeps it (and its data) when it empties.
Delete a persistent room and its stored data. If people are inside it right now the session continues and the room disappears when it empties. LOBBY cannot be deleted. Irreversible — confirm with the developer first.
Delete (soft) one chat message from a room, as the developer. Players see it removed.
Add an email to the developer's account (it stays pending until its inbox owner clicks the confirmation link), or resend that confirmation. Removing emails and changing the primary one are done in the web dashboard.
Read the developer's profile (name shown as app owner, dashboard language, whether a Claude API key is saved, emails on the account) or change `name` / `lang`. Changing the name also updates the owner name on all their apps.
Drafts: a private copy of an app (id "_<appId>") to change files and settings without touching the live app. "info" says whether one exists; "create" makes it (edit it by passing its id to the file/app tools); "apply" promotes the draft over the live app (replaces its files and settings — confirm first; a rooms change that loses data needs confirmRoomsChange); "discard" deletes the draft.
Everything about ONE app: its full config and state (features, rooms, quota and credits today, subscription, gallery, community, ai…), every URL it answers at (by id and slug on each Neblla host and its Cloudflare deploy), the custom domains registered, and whether a TURN relay is available for the P2P "media" option.
The app's resolved community configuration (mode, who publishes, limits, categories, content types, report threshold) plus how much of its community storage cap is used.
Read a room's chat history (newest first, paginated) for moderation. Returns the players' messages in clear: only use it when the developer asks to review a room.
Keep an app files in sync with a GitHub repo. The repo becomes the source of truth: editing the app files in Neblla is blocked while linked (edit in the repo and push). Actions: "repos" — the developer GitHub repos (needs "Sign in with GitHub"); "branches" (url) — branches of a repo; "status"; "link" (url, optional branch, subdir, syncMode manual|auto, token only if the developer has not signed in with GitHub) — first sync, and in auto mode the push webhook is registered automatically when GitHub is linked; "sync" — pull now (only changed files are downloaded); "set_mode" manual|auto; "test_b…
List the app-level moderators (per-app user ids). App moderators manage rooms of any type and moderate the community.
List the gallery reviews of an app (rating 1–5, text, language, author display name, the developer's reply) plus the average, count and 1–5 distribution. Newest first.
List the persistent / predefined rooms of an app with their type, owners, whitelist, moderators, data, message count, last activity and how many users are in each one right now.
The developer's payout account (Stripe Connect), needed to sell products inside their apps. "status": none | pending | active | restricted, with chargesEnabled / payoutsEnabled. "onboarding_link": a one-time URL the developer opens to verify identity and add a bank account (a human must do it). "dashboard_link": a one-time login URL to the Stripe Express dashboard (payouts, balance). Products stay hidden from players until the status is active.
Publish (or replace) the developer's public reply to a review; pass `text: ""` to delete the reply. The reply is shown in the gallery, translated into the reviewer's language when `developerLang` differs. Public text: show the developer the wording before sending.
Set a player's ban state in this app: "none" (lift), "banned" (the app shows the ban message and blocks play), "terminated" (wipes the player's local data in the browser and disconnects them — irreversible for that data), "community_muted" (can play but cannot publish, rate or report in the community). Takes effect live. Confirm with the developer before banned/terminated.
Make a player an app moderator (`moderator: true`) or remove them (`false`). `userId` is the per-app user id (from neblla_list_users), not an email.
Check, set or clear the app's public slug (the readable name in play.neblla.com/<slug>). `checkOnly: true` only tells whether it is valid and free. `slug: null` removes it (old slug links stop working). Changing it changes the public URL — confirm with the developer.
Local backup of an app's files before risky edits. action "create" copies every file to <path>_copy (replacing any previous snapshot); "rollback" restores the copies over the current files (overwrites them — confirm first); "discard" deletes the copies. Use create before a large refactor, rollback if it goes wrong.
Change a room: replace its shared `data` (players in the room receive it live) and/or its `moderators` list. Replacing data overwrites the live game state of that room — confirm with the developer first.
⟨121 unchanged words⟩ the in-app communications inbox feature for end users. `community` configures user-published entries (neblla.community, see the comunidad guide section): { mode: "open"|"review"|"closed", whoPublishes: "users"|"moderators", maxPayloadBytes, categories[], contentTypes[], maxEntriesPerUser, rules }. The community feature itself is enabled with `features: { community: true }` (it needs login).
⟨49 unchanged words⟩ public ID","type":"string"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser, rules }. Merged over the stored config.","properties":{},"type":"object"},"features":{"additionalProperties":true,"description":"Feature switches to merge into the app: { multiplayer, communications, purchases, ai, friends, login, anticheat, community } as booleans. Only the keys you pass change.","properties":{},"type":"object"},"inbox":{"description":"Enable/disable in-app communications inbox ⟨90 unchanged words⟩
Fetch one community entry WITH its payload (the dev-defined JSON), whatever its status.
List the community entries (user-published content, neblla.community) of an app from the backoffice, any status. Use it to triage the moderation queue: status "pending" is what awaits approval in review mode. Returns indexed metadata only (title, summary, category, tags, contentType, owner, status, moderation record); fetch a payload with neblla_get_community_entry. Paginate with `cursor` = the previous `next`.
Moderate a community entry as the app owner: approve or reject a pending entry, remove a published one, or restore a rejected/removed one. `reject` and `remove` require a `reason` from the fixed taxonomy; the author receives the reason and your `message` live (communityUpdated) and in their own list. `remove` keeps the entry and its payload so an appeal can be honoured.
after
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":true,"properties":{"booking":{"additionalProperties":{"additionalProperties":true,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":true,"properties":{"products":{"additionalProperties":{"anyOf":[{"type":"integer"},{"additionalProperties":false,"properties":{"mode":{"enum":["add","fill"],"type":"string"},"n":{"type":"integer"}},"required":["n","mode"],"type":"object"}]},"type":"object"},"unlocks":{"items":{"type":"string"},"type":"array"}},"type":"object"},"type":"object"},"standing":{"additionalProperties":{"additionalProperties":false,"properties":{"series":{"type":"string"},"set":{"type":"string"}},"required":["set","series"],"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe), plus what the service needs: name, minutes, min, max (places), confirmHours, remindHours (1–336: a reminder in each client's inbox that many hours before, once, in their language and with their name; remindText: your own words, with {name} {service} {day} {time} {staff} {site} {for}), resources: [ids], staff: true | [ids], steps: [{ minutes, resources, staff? }], book: \"times\" for free times instead of sessions, chat: true (each session gets a private chat of whoever is booked in it; the business writes in it with neblla_booking chatPost) } and credit kinds { <kind>: { products: { <productId>: credits per unit | { n, mode: \"fill\" } }, unlocks?: [media collection keys | \"*\"], welcome? } } (a purchase of that product tops the balance up, adding; welcome: the credits each new user gets once, the first time they sign in, even if erased and back, e.g. a first class free; { n, mode: \"fill\" } fills it up to n instead, without going over, as a subscription's period «every month you have 20 downloads again»; unlocks: one credit of this kind unlocks one private original of those media collections for good, neblla.media.unlock(id) in the app, «a pack of 5 downloads to choose from Agua»), and standing places { <productId>: { set, series } } (a «plaza fija»: buying that one-time product books its buyer in every case of that repetition up to the end of the month; a subscription, in every case of each paid period; series ids from neblla_booking series { set }). Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at <slug or id>.neblla.app. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"example":{"description":"Mark the app as an example (true) or not (false): it always sells in Stripe's test mode (card 4242 4242 4242 4242, nothing charged, purchases marked test and counted nowhere but giving what they give), its slug ends in -example, it shows as an example in the gallery, and at each neblla_promote its code goes to the developer's public GitHub repo of examples. Confirm with the developer: the code becomes public and the address changes","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser, replyTo, welcome: { text (string or { <lang>: text }, {name} placeholder), name? } } (only the keys given change; welcome null removes it); the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}after
{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"addDomain":{"description":"Append a domain entry (URL or origin) to the allowed list. A wildcard allows one subdomain level: \"*.platform.com\" (for platforms that serve each game from its own subdomain); \"*\" or \"*.com\" are refused (invalid_domain).","type":"string"},"ai":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"AI config, replaced whole: { provider: \"claude\" (default) | \"openai\" | \"deepseek\", model (the provider's: opus-latest | sonnet-latest | haiku-latest, gpt-5 | gpt-5-mini | gpt-5-nano, deepseek-chat | deepseek-reasoner; with your key it is a ceiling, a user may ask for a cheaper one of the same provider), maxOutputTokens, userTokenMode (\"off\": your key for the provider, saved in your Neblla account; \"browser\": the user's key, called from the page, not for DeepSeek; \"server\": the user's key through Neblla), presets, limitsEnabled, tokensPerDay } — pass null to disable"},"appId":{"description":"The app public ID","type":"string"},"auth":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},{"items":{"additionalProperties":false,"properties":{"audience":{"type":"string"},"issuer":{"type":"string"},"jwksUrl":{"type":"string"}},"required":["issuer"],"type":"object"},"type":"array"}]},{"type":"null"}],"description":"Your own login without a server (BYOAuth): your auth provider (Auth0, Clerk, Supabase Auth, Firebase, Cognito… any issuer of JWTs) whose token the front passes to neblla.auth.login({ jwt }); its `sub` is the externalId of the user. { issuer (https), jwksUrl? (else OpenID discovery), audience? }, or a list of up to 5; null clears it"},"business":{"anyOf":[{"additionalProperties":false,"properties":{"booking":{"additionalProperties":{"additionalProperties":false,"properties":{"cancelHours":{"type":"number"},"cost":{"type":"integer"},"creditKind":{"type":"string"},"pay":{"enum":["booking"],"type":"string"},"product":{"type":["string","null"]}},"type":"object"},"type":"object"},"credits":{"additionalProperties":{"additionalProperties":false,"properties":{"products":{"additionalProperties":{"type":"integer"},"type":"object"}},"type":"object"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"Business templates: booking sets { <set>: { cancelHours (deadline to cancel with a refund), cost (credits per booking, 0 = free), creditKind } or, paid each time at booking, { cancelHours, pay: \"booking\", product: <one-time productId> } (the place is held 30 minutes while the user pays; cancelling in time refunds the money in Stripe) } and credit kinds { <kind>: { products: { <productId>: credits per unit } } } (a purchase of that product tops the balance up). Slots: neblla_booking. Replaces the whole object; null clears it"},"community":{"additionalProperties":true,"description":"Community config (user-published entries): { mode, whoPublishes, maxPayloadBytes, categories, contentTypes, maxEntriesPerUser (0 = no limit, the default), reportThreshold (3 by default), rules, prescreen: { mode: off|flag|hold, images }, downvotes (true: users may also vote against; false by default), fields: { name: string|number|integer|boolean|date } and index: [names] — your own indexed fields: publish({ fields }), list({ where: { f: { gte, lt… } }, orderBy: \"-f\" }) }. Merged over the stored config.","properties":{},"type":"object"},"compiled":{"description":"A compiled app (Capacitor, Ionic, Tauri, Electron): take its origins without a domain","type":"boolean"},"confirmRoomsChange":{"description":"Set true to apply a rooms change that discards saved rooms data (only after the developer agreed)","type":"boolean"},"connection":{"anyOf":[{"additionalProperties":false,"properties":{"clock":{"anyOf":[{"additionalProperties":false,"properties":{"max":{"maximum":86400,"minimum":10,"type":"integer"}},"required":["max"],"type":"object"},{"const":false,"type":"boolean"}],"description":"The common clock (neblla.room.now()): after the first samples it is measured every 1 s, 1.4 s, 2 s, 2.8 s… (×√2) up to max seconds (3600 by default). A fast game may lower it (e.g. 120); false measures it only when the session starts (a chat can trust the device clock)"},"presence":{"additionalProperties":false,"description":"\"Are you still there?\": the SDK sends a heartbeat every `every` seconds and after `timeout` without one Neblla cuts the connection and keeps the player's place for 3 minutes (12 s if they were the room's server, who hands over at once); only then do they leave the room (4 × every by default, at least 2 ×). Defaults: an app with the room module 15/45, any other 120/600. A hidden page always gets 180 s","properties":{"every":{"maximum":600,"minimum":5,"type":"integer"},"timeout":{"maximum":3600,"type":"integer"}},"required":["every"],"type":"object"}},"type":"object"},{"type":"null"}],"description":"How often devices keep in touch with Neblla; null goes back to the defaults"},"connections":{"description":"How devices talk. p2p (what a new app is born with; an app from before with no value reads as all): direct, then the TURN relay if p2p.turn is on; a device that reaches nobody is left out (the user is told, the app gets room.on(\"leftOut\"), the panel counts them). all: the same, and Neblla's server forwards messages as the last resort (each copy costs quota). server: only through Neblla, never WebRTC. none: neither (and the messages of a room are not kept in its history).","enum":["all","p2p","server","none"],"type":"string"},"customDomain":{"description":"Your own domain for the app Neblla hosts (e.g. \"mijuego.com\"): the answer gives the CNAME to point it at; Neblla gets the certificate and serves the app there, as at play.neblla.com/<slug or id>. Asking again reports the status (pending → active). null removes it","type":["string","null"]},"erasureWindow":{"description":"neblla.auth.requestErasure opens a Neblla window where the user confirms (default true); false if you handle erasure requests your own way","type":"boolean"},"features":{"additionalProperties":true,"description":"The app's neblla.* modules to switch, as booleans: auth and db (always on), message, state, room, rooms (what multiplayer means), social, shop, inbox, ai, api, anticheat, groups, community, business, tools. Only the modules you pass change; turning one on brings what it needs (message, state, rooms and anticheat need room) and turning one off takes what needs it. Whatever is off is not in the SDK the app downloads, the server refuses it and the guide (neblla_get_llms with appId) leaves it out. An old switch name (multiplayer, friends, purchases, communications, login) is refused with unknown_module.","properties":{},"type":"object"},"fog":{"anyOf":[{"additionalProperties":false,"properties":{"latencyWatch":{"description":"Flag impossible send times (little quota)","type":"boolean"},"window":{"description":"ms a message stays provisional before it is final (default 1000; an action game wants ~200)","maximum":5000,"minimum":50,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Fog settings; only the fields passed change, null resets them"},"groups":{"anyOf":[{"additionalProperties":false,"properties":{"clientCreate":{"type":"boolean"},"creatorRole":{"type":"string"},"invitable":{"items":{"type":"string"},"type":"array"},"maxMembers":{"type":"integer"}},"type":"object"},{"type":"null"}],"description":"Groups the users make themselves (neblla.groups.create / invite / join / leave): clientCreate on, the role the creator gets (creatorRole, who invites), maxMembers, and the roles an invitation may give (invitable). null: no groups from the client"},"inbox":{"anyOf":[{"type":"boolean"},{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Inbox: true/false, or { maxPerUser } to lower the per-user cap within the plan ceiling; the inbox itself is the inbox module, features.inbox"},"keepFogCases":{"description":"With BYODB the Fogs' cases are yours (table nb_fog_cases in your database) and are deleted on their own 30 days after they were opened. true: they stay until you delete them. false or null: deleted at 30 days (the default). Without BYODB it does nothing: Neblla keeps them 30 days","type":["boolean","null"]},"keys":{"additionalProperties":{},"description":"Public values the app reads with neblla.api.key(name): text, number, yes/no or JSON. Merged into the current keys; null removes one. Never a secret: anyone who opens the app can read them.","type":"object"},"limits":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"maxBytesPerUser":{"type":"integer"},"playerQuota":{"maximum":100,"minimum":0,"type":"integer"},"tables":{"additionalProperties":{"additionalProperties":false,"properties":{"maxRowsPerPlayer":{"type":"integer"}},"required":["maxRowsPerPlayer"],"type":"object"},"type":"object"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"What a purchase raises, per product id: { premium: { maxBytesPerUser, tables: { notas: { maxRowsPerPlayer } }, playerQuota } }. playerQuota: the % of the app's daily quota a buyer may spend, instead of the app's playerQuota (0 = no cap). A user who owns the product (bought, or a live subscription) gets them; without it, the app's own. Owning several, the most generous wins; 0 = no limit"},"matchmaking":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"No-code matchmaking config: { mode: \"nocode\"|\"custom\", rule: \"count\"|\"timer\"|\"count_then_timer\"|\"button\", minPlayers, maxPlayers (0=unlimited), waitSeconds, allowPresenterStart (true by default: the presenter may also start the room with neblla.room.start(); false in nocode: only gathering.js does, the presenter gets not_authorized) }. Fields you leave out keep their saved value. In \"nocode\" mode (the default) this regenerates server/gathering.js from the rules; in \"custom\" mode you own that file — set it before writing gathering.js by hand."},"moderationLog":{"description":"The moderation log, off by default: true writes down every moderation act (who named or removed a moderator, each ban, every message or community entry someone other than its author took down), who did it and when; read it with neblla_get_moderation_log or in the back office. Each line costs quota and fills the app's store (with BYODB it goes to your own database). false or null: off, nothing more is written","type":["boolean","null"]},"name":{"description":"New display name","type":"string"},"p2p":{"anyOf":[{"additionalProperties":false,"properties":{"allow":{"description":"false = no WebRTC at all (default true)","type":"boolean"},"turn":{"description":"true = offer Neblla's TURN relay when direct P2P fails; relayed bytes spend quota (default false)","type":"boolean"}},"type":"object"},{"type":"null"}],"description":"P2P settings: { allow, turn }. The SDK has no audio or video."},"pattern":{"description":"App pattern id","type":"string"},"permissionJustifications":{"additionalProperties":true,"description":"Why the app asks for each SSO scope, in one sentence the USER reads on the consent screen: { email, friends, discoverable }. Keyed by scope; any other key is stored but never shown. NOT browser permissions (microphone/camera/etc. are not read from here).","properties":{},"type":"object"},"playerQuota":{"anyOf":[{"maximum":100,"minimum":0,"type":"integer"},{"type":"null"}],"description":"The most one user may spend of the app's daily quota, in % (20 by default; 0 = no cap; null = back to the default), so one user alone cannot use it up. Past it, that user's events are refused with the error player_quota_exceeded (neblla.app.on('error')) until 00:00 UTC; the rest keep playing. The room's server is never capped."},"playerStats":{"description":"Your players' visit statistics, off by default: true keeps, for each player, when they first came, when they last came and how many times, in a table of the app's store (nb_player_stats, id = the player's user.id; with BYODB in your own database, created on the first visit). It is yours: Neblla keeps its own measurement apart and never reads it. false or null: off, nothing more is written (what was written stays)","type":["boolean","null"]},"purchases":{"anyOf":[{"additionalProperties":false,"properties":{"emailCheckout":{"type":"boolean"}},"type":"object"},{"type":"null"}],"description":"emailCheckout (on by default): a guest buys with just the email Stripe asks for — a light account on that device, no password; on another device neblla.shop.restore(email) mails a link that brings the purchases; signing in later with a Neblla account of that confirmed email takes them into it. false: only signed-in users buy, and neblla.shop.buy() from a guest opens the sign-in in the checkout window and goes on to the payment once they are in"},"removeDomain":{"description":"Remove a domain entry exactly matching this string","type":"string"},"roles":{"anyOf":[{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object"},{"type":"null"}],"description":"Roles for groups (neblla_group): each role's rights over each table, \"read\", \"write\" or \"read write\". e.g. { equipo: { tareas: \"read write\" }, cliente: { tareas: \"read\" } }. A row saved with { table, group } is read and written by the group's members as their role says. The back office team (neblla_team) can take a role too. Replaces the whole set; null clears them"},"rooms":{"additionalProperties":true,"description":"Rooms policy object (same shape the app returns in `rooms`)","properties":{},"type":"object"},"savedReplies":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["title","text"],"type":"object"},"type":"array"},{"type":"null"}],"description":"The inbox's saved replies (the developer picks one when answering a user): [{ id?, title (≤ 80), text (≤ 5000) }], ≤ 50, the whole list; keep the ids of the ones that stay. The app's own: it applies at once to the published app and its pre, nothing to promote. null clears them"},"saves":{"anyOf":[{"additionalProperties":false,"properties":{"maxBytesPerUser":{"description":"Most bytes one user can keep with db.save() (10240 by default; 0 = no limit). An anti-abuse limit, so nobody tampering with the client fills your store with their own things.","minimum":0,"type":"integer"}},"type":"object"},{"type":"null"}],"description":"User saves; null goes back to the defaults"},"scoreRenames":{"additionalProperties":{"type":"string"},"description":"With scores: the boards renamed in this call, { oldName: newName } (the old one gone from scores, the new one in it). Their scores move to the new name: the draft's at once, the published app's when this reaches it (neblla_promote). Without it, a new name is a new empty board and the old one is deleted with its scores","type":"object"},"scores":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"check":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"},"secret":{"type":"string"}},"required":["secret"],"type":"object"},{"type":"null"}],"description":"Your own validation: Neblla POSTs { event: \"score\", board, player, value, data, run: { id, startedAt, seconds, seed, day } } to that callApi secret (neblla_set_api_secret) + path, and your code answers { ok: true } or { ok: false, reason }; no answer: check_unavailable and the game can be sent again"},"integer":{"description":"Only whole numbers (else invalid_value)","type":"boolean"},"max":{"description":"The most a score can be; over it, refused (over_max)","type":"number"},"maxPerSecond":{"description":"The most points per second of game (else too_fast)","type":"number"},"maxSeconds":{"description":"How long a game can be sent after it started (86400 by default, up to 7 days; later: run_expired)","type":"number"},"min":{"description":"The least a score can be; under it, refused (below_min)","type":"number"},"minSeconds":{"description":"The shortest a real game can last; a score sent sooner after start is refused (too_soon)","type":"number"},"order":{"description":"desc (default): more is better (points); asc: less is better (tries, milliseconds)","enum":["asc","desc"],"type":"string"},"runsPerDay":{"description":"Games a user may start per day and board (1–1000; none = no limit)","type":"integer"},"seed":{"description":"run (default): a seed per game; daily: the same seed for every player all day (UTC), e.g. the word of a daily puzzle","enum":["run","daily"],"type":"string"}},"type":"object"},"type":"object"},{"type":"null"}],"description":"Rankings of a game for one, checked by Neblla before a score enters (neblla.scores.start(board) → run; neblla.scores.submit(run, value, { data }) → { ok, best, place } or { ok: false, reason }; neblla.scores.top(board, { period: \"all\"|\"day\" })). { board: { order, min, max, integer, minSeconds, maxSeconds, maxPerSecond, seed, runsPerDay, check } }; names a-z0-9_, up to 20. What is refused does not enter, goes to the app log (Build › Errors and refusals) and, if only a cheat explains it, marks the user. Replaces the whole set; null clears it. A board left out is deleted, and its scores with it (the draft's at once, the published app's when this reaches it with neblla_promote); to rename one and keep its scores, say so in scoreRenames"},"server":{"additionalProperties":false,"description":"Server (\"own\"): { fallback: \"fog\" } (the default) lets Fog take the room with the same room.js when the developer's server is gone, and gives it back when the server returns; { fallback: null } makes the room wait for it","properties":{"fallback":{"anyOf":[{"enum":["fog"],"type":"string"},{"type":"null"}]}},"type":"object"},"serverMode":{"description":"Where server/*.js runs: js on a user's device (Fog); own on the developer's server (npx @neblla/server, see neblla_server_key); cloud in Neblla's cloud (a box per app that sleeps when idle; saving a server/ file redeploys it; billed by awake time and messages)","enum":["js","own","cloud"],"type":"string"},"tables":{"anyOf":[{"additionalProperties":{"additionalProperties":false,"properties":{"fields":{"additionalProperties":{"enum":["string","number","integer","boolean","date"],"type":"string"},"type":"object"},"index":{"items":{"type":"string"},"type":"array"},"maxRowsPerPlayer":{"type":"integer"},"server":{"type":"string"}},"required":["fields"],"type":"object"},"type":"object"},{"type":"null"}],"description":"The app tables: rows each tied to one user (neblla.db.save(data, { table }) / neblla.db.load({ table, where })). { name: { fields: { field: type }, index: [fields], maxRowsPerPlayer? (500), server?: \"write\" (a real server, own or cloud, and the admin API may write rows for a user; a Fog never) } }; names a-z0-9_, types string|number|integer|boolean|date. Replaces the whole set; a field added later becomes a new column (none is dropped). Queries across players: neblla_table_query, room.js, Automations. null clears them"},"theme":{"anyOf":[{"additionalProperties":true,"properties":{},"type":"object"},{"type":"null"}],"description":"Theme: { palette, typography, layout }"},"trade":{"anyOf":[{"additionalProperties":false,"properties":{"days":{"type":"integer"},"products":{"items":{"type":"string"},"type":"array"},"tables":{"items":{"type":"string"},"type":"array"},"with":{"enum":["friends","all"],"type":"string"}},"type":"object"},{"type":"null"}],"description":"What users may trade between them (C170): neblla.db.offer({ to, give, get }) → the other accepts with neblla.db.acceptOffer(id), and everything moves at once or nothing does. { tables: [tables whose rows change owner], products: [product ids whose units (one-time or consumable, never a subscription) pass from one user to the other], with: \"friends\" (default: only Neblla friends) | \"all\", days: how long an offer lasts (1–30, 3 by default) }. A real-money product is tradable only if you list it; the payment stays with the buyer (sales and refunds do not move). The log: neblla_trades. Replaces the whole object; null: nothing is tradable"},"turnstile":{"anyOf":[{"anyOf":[{"type":"boolean"},{"additionalProperties":false,"properties":{"secretKey":{"type":"string"},"siteKey":{"type":"string"}},"required":["siteKey"],"type":"object"}]},{"type":"null"}],"description":"Turnstile before a new guest: true (Neblla's widget), { siteKey, secretKey } (the developer's own), false/null (off)"},"visible":{"anyOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"},"tables":{"additionalProperties":{"enum":["all","friends"],"type":"string"},"type":"object"}},"type":"object"},{"type":"null"}],"description":"What other users may see of a user's saves (C169), read with neblla.db.dataOf(user) and neblla.db.loadOf(user, { table, where… }): { data: { <path of db.data>: \"all\" | \"friends\" }, tables: { <table>: \"all\" | \"friends\" } }. Paths are top-level keys or dotted (\"perfil.nombre\", up to 5 levels, 50 paths); \"all\" is any user of the app, a guest too; \"friends\" is the user's Neblla friends (and the user). Nothing that is not marked is ever shown. Replaces the whole object; null: nothing is visible"},"webhooks":{"anyOf":[{"items":{"additionalProperties":false,"properties":{"event":{"enum":["purchase","refund","register","erasure_request","subscription_end","ban","flag","community_report","booking"],"type":"string"},"path":{"description":"Path under the secret’s base URL (default /)","type":"string"},"secret":{"description":"An API secret (neblla_set_api_secret, the one neblla.api.call uses): its base URL, headers and limits","type":"string"}},"required":["event","secret"],"type":"object"},"type":"array"},{"type":"null"}],"description":"Outgoing webhooks: Neblla POSTs { event, appId, at, data } to the secret’s URL when the event happens (users by their id in this app). Replaces the whole list; null clears it. Deliveries: neblla_list_webhook_deliveries"}},"required":["appId"],"type":"object"}