How it works
How grok bubbles works, without AI
Grok is only the trigger. A normal program turns the link into a working stack. The files are written by code from a fixed menu, checked by a validator, built by CI and run on a runner. No model writes any of it.
Contents22 min read
- The idea in one minute
- What Grok does, and what it does not do
- The link, piece by piece
- The word menu
- How the service.json files are made
- The build pipeline, step by step
- The live run
- Safety
- Reliability and honest limits
- Why there is no AI in the middle
- Architecture
- The JSON API
- Frequently asked questions
- Try it
The idea in one minute#
You reply to a post on X with the trigger, @grok bubbles. Grok answers with a link. The link is the product: a web address made of a handle and a few words, such as demo.flisk.sh/yourhandle/solidity-dapp-with-postgres-and-email. Open it and you get a demo of a stack built from those words, with a frontend, an api, a database and maybe a worker, a chain node or an agent. The demo page lists every service.json file, the commands to run them, and a repo that CI built and checked. When a live slot is free, the stack also runs for a couple of hours.
Everything after the words is ordinary software. A Cloudflare Worker reads the words, matches them against a menu of parts, renders the files, checks them against rules and stores the result. A GitHub Actions run then creates the project with the Flisk command line tool and pushes a branch. A second workflow starts the project on a runner with Docker and Tilt, and two tunnels make it reachable for a while. No step asks a language model what to do.
What Grok does, and what it does not do#
Grok picks a handle and a few words from the thread and writes a link, or calls the JSON API with them. That is all. Grok never chooses a service, never writes a service.json file, never writes a command and never names a version. The Worker validates the handle and runs the same plan code it would run for a person typing the link by hand.
The reply text is built by code as well. It is made from the finished plan, starts with the handle, lists the service names joined with a middle dot and ends with the link. It stays under 280 characters. For a named platform it says either that the wiring is already there or that a request was logged.
The time the model made things up
This is the true story, and it is why the files are written by code. Early on we let Grok answer on its own, with a free hand to explain how to run a demo. It wrote fluent, confident instructions, and some were wrong. It invented a command that does not exist, npx flisk-sh/flisk demo, and it gave an install command that does not exist either. Someone who copied either one would fail at the first step.
The fix was not a better prompt. It was to take the writing away from the model. Commands and files now come from code. The validator still carries the bad strings as forbidden terms, so a plan that contains npx flisk-sh/flisk demo, npm i -g flisk-sh/flisk or npm install -g flisk-sh/flisk fails validation before anything is built.
The link, piece by piece#
A demo link has the shape demo.flisk.sh/handle/words. The handle is the X handle without the at-sign. The Worker accepts 1 to 15 letters, digits or underscores, and returns a 400 error for anything else. A leading at-sign in the path is accepted and dropped. The words segment is optional, so a bare handle opens a default stack.
The words are lowercase and joined with dashes. The Worker turns dashes, underscores and plus signs back into spaces before planning. A words segment longer than 300 characters does not match the link route at all. The JSON API cuts the text at 2,000 characters.
The id of a demo is built from the handle, the stack name, a hash of the summary and the vendor name, so it looks like yourhandle-dapp-3fa9c2. The same person asking for the same stack gets the same demo back, even in other words or days later. Two different people never share a demo. A handle does not collect copies of one stack.
Words are matched one by one, by whole word, so cart matches and cartoon does not. Word order does not matter, and words that match nothing are ignored. One consequence: the route for /how is registered before the handle route, so a person whose handle is how cannot be opened as a link.
The word menu#
The menu is a table in src/plan.js. A group matches when any of its words appears in the text. Each group switches on fixed parts. The menu can never add a service type that the plan code does not know, and the renderer copies only keys it knows. The table below is copied from the source, and a test checks each word against that file.
| Group | Words that match | What the plan does with it |
|---|---|---|
| Chain | solidity, contract, contracts, evm, foundry, anvil, hardhat, onchain, web3, dapp, nft, defi, token, mint | A bring-your-own chain service (a local Anvil node, experimental) and an indexer worker that reads its events. Without another stack word, the stack becomes dapp. |
| Indexer | indexer, index, events, subgraph | Turns the database on. An indexer service appears only together with a chain word. |
| Database | db, database, postgres, postgresql, sql, prisma, users, orders, catalog, profiles, analytics, crud, inventory | Adds the Prisma feature to a Bun api. Python apis get a note about migrations instead. |
| Jobs | worker, workers, jobs, queue, background, email, emails, notifications, webhooks, retries, cron, nats | A jobs worker service with the nats feature. It also turns the database on, and waits for the migrator. |
| Cache | redis, cache, caching | The redis feature on the api. |
| Realtime | websocket, websockets, realtime, chat | The websocket feature on the api. |
| Agent | agent, agents, llm, ai, assistant, rag | An assistant service of type agent, with a health check and a port. |
| MCP | mcp | A tools service of type mcp, served over Streamable HTTP at /mcp. When an agent is named too, the assistant depends on it. |
| Commerce | shop, store, storefront, ecommerce, commerce, checkout, cart, b2b, headless | Sets the stack to store. A chain word wins over it, and a named vendor sets store too. |
| Frontend | react, vue, svelte, solid, preact, lit, qwik | The web service framework. With none named, the plan uses React and adds a note saying so. |
| Language | python, fastapi, django, flask (Python); go, golang (Go) | The api language. Any other api is Bun with Hono, and only Bun apis get the Prisma migrator. |
| Vendor | a name before “API”, or after “connected to”, “integrates with”, “wired to”, “hooked up to”, “on top of”; or the word thor | Thor gets the hand-built wiring and its env list. Any other name is logged as an integration request and gets placeholder env lines. |
src/plan.js.Some parts pull in others. Any jobs word turns the database on, because the Flisk Board keeps its items in Postgres. A chain word adds an indexer that depends on the chain node and the migrator. A Bun api always gets a database, whatever the words say. A Python api does not, so for Python the database words only add a note about running migrations as a one-shot job. The chain service is marked experimental in the plan, and its note says it has not yet run end to end with flisk up.
Vendors are a separate rule. The plan looks for a name in front of the word API, or after a connecting phrase such as connected to. Common words that would be false names, such as the or graphql, are excluded. Thor Commerce is the one name with hand-built wiring, so the plan links that repository and its environment list. Any other name is stored as an integration request with status requested, and a person builds it by hand.
Six worked examples
These rows are rendered from buildPlan each time the page is served, so they match the code you are reading.
| Words and link | Stack | Services | Platform |
|---|---|---|---|
solidity dapp with postgres and emaildemo.flisk.sh/yourhandle/solidity-dapp-with-postgres-and-email | dapp | web (frontend), api (backend), api-migrator (migrator), jobs (worker), chain (bring-your-own), indexer (worker) | none |
vue storefront connected to saleordemo.flisk.sh/yourhandle/vue-storefront-connected-to-saleor | store | web (frontend), api (backend), api-migrator (migrator) | saleor, requested |
ai agent with mcp toolsdemo.flisk.sh/yourhandle/ai-agent-with-mcp-tools | agents | web (frontend), api (backend), api-migrator (migrator), tools (mcp), assistant (agent) | none |
python fastapi notes with redisdemo.flisk.sh/yourhandle/python-fastapi-notes-with-redis | app | web (frontend), api (backend) | none |
react store connected to thordemo.flisk.sh/yourhandle/react-store-connected-to-thor | store | web (frontend), api (backend), api-migrator (migrator) | Thor Commerce, ready |
websocket chat with redis and cron jobsdemo.flisk.sh/yourhandle/websocket-chat-with-redis-and-cron-jobs | app | web (frontend), api (backend), api-migrator (migrator), jobs (worker) | none |
The first row names chain and database words, so the stack is dapp, and the chain node and its indexer appear. No frontend is named, so the plan uses React and adds a note. The second row names Saleor, which has no hand-built wiring, so it is recorded as a request. The fourth row is Python: redis adds the cache feature, and no Prisma migrator appears. The fifth row is Thor, the one platform with wiring, so its status is ready.
How the service.json files are made#
Each service in a plan has a manifest, the object that becomes its service.json. renderManifest in src/plan.js writes the schema link, the schema version, the app name, the app type and the stack, then copies only the fields the plan set: port, framework, language, image, health check path, params, dependencies and features. A field the plan does not set is absent from the file.
Two checks run before anything is built, and they are deliberately simple. The first is src/validate.js, which enforces the rules of the Flisk schema that a generated demo could break. The app type must be one of the nine the schema knows, from backend and frontend to bring-your-own. A frontend port must be 3000 to 3999. A backend or bring-your-own port must be 4000 to 5999. Every port must be a whole number from 3000 to 9999, and no two services may share one. A worker must have the nats feature. A bring-your-own service must have an image. Every dependsOn name must be a service in the plan. A failed check saves the demo as failed, with the errors, and no build starts.
The second check runs in CI with the real tool. After the files are written, the build runs flisk config verify. A file that passes our rules but not the CLI's is caught there, and the run fails with the errors.
A real service.json
This is the manifest the plan renders for the api in the Saleor example, exactly as the function returns it.
{
"$schema": "https://flisk.sh/schema.service.json",
"schemaVersion": 1,
"appName": "api",
"appType": "backend",
"stack": "store",
"port": 4000,
"healthCheckPath": "/health",
"dependsOn": [
"api-migrator"
],
"featuresEnabled": [
"prisma"
]
}The file in the repository is a little larger. CI first runs flisk resource for each service, which writes the file the CLI expects, and then merges the plan's manifest over it, so the plan wins where the two overlap. Keys the CLI sets and the plan does not are kept.
The commands
The commands on the demo page come from the same plan, through the same argument builder CI uses. For the Saleor example they are these.
npm install -g @flisk-sh/flisk flisk doctor flisk project --yes flisk resource web --type frontend --stack store --framework vue --yes flisk resource api --type backend --stack store --feature prisma --yes flisk up store flisk status
The build pipeline, step by step#
The pipeline has seven hops. Each one is a small piece of code with one owner.
- The Worker plans.
createOrReuseinsrc/index.jsturns the words into a plan, validates it and writes the record. If the id already exists, the existing record is returned and nothing else happens. - The Worker dispatches. A new record with status
buildingstarts a run of the build workflow in the Worker repository, with the event typeflisk-demo. The payload is the demo id and a callback address built from the Worker's own origin. It carries no thread text. - CI reads the plan.
.github/workflows/build-demo.ymlinstalls Node, Bun and the Flisk CLI, then runsscripts/build-demo.mjs. The script reads the id from the event payload, fetches the plan from the Worker and gives the job a ten minute limit. - CI scaffolds.
scripts/scaffold.mjscreates a folder named after the stack with the word demo, because the folder name becomes the project name and so part of every local URL. It runsflisk project --yes, thenflisk resourceonce for each service the plan owns. Every call goes throughexecFileSyncwith an array of arguments. - CI overlays and checks. The generated files are matched by app name against the plan and written back with the plan's manifest merged over them. If a planned service is missing, the build fails. For a Vue or React frontend with a Bun api, CI also writes the Flisk Board, a small app on the api. A
.env.exampleis written when the plan has environment lines. Thenflisk config verifyruns, andflisk up --dry-runprints the real local URLs, which are read from its text. - CI pushes the branch. The folder is committed to a branch named
demo/plus the id and pushed with force to the public repositoryflisk-sh/demos. A rebuild replaces the branch, so each build starts from a fresh history. A visitor clones that branch. - CI starts the live run and reports back. The build dispatches
live-demowith the id to the demos repository, then posts the result to the callback address: status, repo, checks and local URLs. If the live dispatch fails, the build still succeeds and says so in its log.
The live run#
A second workflow, .github/workflows/live.yml in the demos repository, runs one demo for real. It runs on a GitHub-hosted Ubuntu runner, which has Docker. It first checks the id against a strict pattern, checks out the ops scripts from main and the demo branch, and renames the folder to the stack with -demo.
Next it claims a live slot from the Worker. A claim is refused when every slot is taken, and that is not an error: the demo stays a static page with its plan and repo. When the claim works, the run installs Tilt, cloudflared and the Flisk CLI at pinned versions, makes the folder writable for the containers, and runs flisk up under a thirty minute limit. A failing resource does not stop the run, because Tilt stays up and shows the failure.
The two tunnels
The run opens two Cloudflare quick tunnels. The Tilt tunnel goes through scripts/readonly-proxy.mjs, which forwards only GET and HEAD requests and websocket upgrades made with GET. Anything else gets a 403, so a visitor can watch Tilt but cannot start, stop or edit a resource. The app tunnel goes through scripts/app-proxy.mjs, which keeps calls to the api on the same origin as the page.
The run then tells the Worker the two addresses and the end time. The Worker accepts only trycloudflare addresses and keeps them in the slot, not in the record. The demo page frames the app, and loads Tilt only when that tab is opened.
How long, and how many
A live run lasts 120 minutes by default, set by a repository variable. The Worker caps any end time at six hours after the claim. The workflow keeps the stack up for at most 330 minutes, inside a job limit of 355 minutes, because GitHub kills any job at six hours. The slot expires after the run plus a grace period, and a final step releases the slot and stops the stack whether earlier steps passed or not. The Worker allows up to 20 live demos at once.
Safety#
The system takes text from strangers, so each place where that text could do harm has a rule.
Handles must match the pattern of 1 to 15 letters, digits or underscores, on both the link route and the JSON route. Ids are lowercased, so two spellings of one handle cannot collect two demos.
Thread text never reaches a shell. The words are data. They are matched against the menu and never placed in a command. The CI scripts pass every argument to the Flisk CLI as an array. The dispatch carries the id and the callback address, not the words, and the CI script reads the payload from the event file. The live workflow checks the id before any step uses it.
Everything rendered is escaped. Every value on a page goes through one escaping function that turns ampersands, angle brackets, quotes and apostrophes into entities. A demo whose words contain markup is shown as text.
Tokens live only in secrets. The Worker keeps its GitHub, callback and admin tokens as Wrangler secrets. The build and live workflows read their tokens from the repository secrets. The step that runs the demo's own code has no secrets in its environment, and the workflow says so in a comment.
Limits per hour. The production configuration allows 60 CI builds an hour across all handles, and 10 for one handle. The counts live in the Cache API of the data center that served the request, so they are approximate, which is fine for stopping a storm. Over the cap, a demo waits and the sweep retries it later.
Reliability and honest limits#
A scheduled sweep runs every five minutes. It retries failed demos, demos that are over the build cap, and ready demos with a repo that are not live. A failed demo is retried after ten minutes, up to six times. At most eight retries run in one sweep. A build that has not reported back after fifteen minutes is dispatched again once. None of these retries asks a model for a new plan. It runs the same plan again.
The records live in Cloudflare KV on the free plan, which has a daily write limit. When the limit is hit, writes fail until midnight UTC. Every route is wrapped, so the visitor gets a saving-is-paused page with the time left. That page still shows the full plan, because a plan needs no storage to compute. The reply says that the repo and live run come back after the reset. A CI result that the Worker refuses for the same reason is logged as a limit, not as a failed build.
Flisk is a local development tool. It runs a stack on Docker with Tilt on your own machine. It is not a deploy tool, and a demo is not a production deployment. Live runs are temporary, two hours by default, on shared runners.
Platform integrations are built by hand. Thor Commerce is the only one wired in the code. Any other platform becomes a request for a person to build, and building one took days for Thor.
The chain service, Anvil, is experimental. Frontends are Vite based, and the frontend types the plan knows are the listed frameworks. Next.js is not a Flisk frontend type, so it is not offered. The validator and the CLI check that a stack can start. They do not read your application code.
Why there is no AI in the middle#
A model would make this worse in four ways. The first is determinism. The same words and the same menu give the same plan, so a link can be given to a colleague and it means the same stack. A model would answer differently on different days.
The second is validity. The output must be a file that the Flisk CLI accepts, every time. A renderer that copies only known keys cannot write a forbidden key, and a validator can check every port and dependency before a build starts. A model that is right nine times in ten still fails one demo in ten.
The third is cost and speed. A link opens instantly, and the plan needs no network call. There is no per-request model cost, no model outage, and no provider rate limit between a link and its page.
The fourth is audit. Every output traces back to a menu word and a line of code. When something looks wrong, a person can read the path in a few minutes.
AI can still help later without breaking this. The menu is the seam. A model could read a thread and choose words, or choose parts, but only from the same menu. The plan would still validate the choice, and CI would still write and check the files. The model would be a better trigger, never the author.
Architecture#
The pipeline has nine stages, from the post to the visitor. The Worker owns the plan and the record, the build workflow owns the scaffold and the branch, the demos repository owns the live run, and the proxies decide what a visitor can reach.
The second diagram draws the decision: from words, through the menu, to the files.
The JSON API#
GET /v1/demo asks for a demo. The parameter user is the handle, with or without the at-sign. The parameter q is the text describing the stack, and the older name context is also accepted. parts adds words to the text before planning. stack sets the stack name, which must be 2 to 20 lowercase letters, digits or dashes, starting with a letter.
curl "https://demo.flisk.sh/v1/demo?user=yourhandle&q=solidity+dapp+with+postgres"
A new demo returns status 202. An existing one returns 200 with its record. While saving is paused, the answer is 200 with the header x-demo-mode: preview. An invalid handle returns 400.
The response carries instructions, a sentence telling a model to post the reply as written. It also carries reply, url, id, status (building, ready or failed) and plan. Once a build reports back, readyAt, repo, checks and urls appear. liveNow is true while a slot holds a live run, and live then holds the two tunnel addresses and the end time. To read a demo again, call GET /v1/demo/<id>.
Frequently asked questions#
Does Grok write the service.json files?
No. Grok picks a handle and some words. Code renders the files from the plan, after the validator has checked it.
Does a language model run anywhere in the build?
Not in the code that builds a demo. The Worker, the scripts, the workflows and the live runner call the Flisk CLI and fixed functions only.
Why does the same request give the same demo?
The plan is a pure function of the words and the menu, and the id is built from the handle, the stack and the summary. A repeat request returns the record that already exists.
Can I choose the stack myself?
Yes, with the stack parameter. Otherwise the stack comes from the words: chain first, then commerce or a vendor, then agents, then app.
How long does a live run last?
Two hours by default. The Worker caps any end time at six hours after the claim.
How many demos can run live at once?
Twenty. When every slot is taken, the demo stays a static page with its plan and repo.
What happens if a build fails?
The record is saved as failed with its errors. The sweep retries it up to six times. A demo that fails validation never starts a build.
Why does the page say saving is paused?
The free storage plan has a daily write limit. Saving stops until midnight UTC, and the page still shows the full plan.
Is Flisk a deploy tool?
No. Flisk runs a stack locally with Docker and Tilt.
Can a demo use Next.js?
No. Flisk frontends are Vite based, and Next.js is not offered. Asking for it falls back to the default frontend.
Why is Thor the only platform with wiring?
Its integration was built by hand, and the plan links that repository. Other platforms are logged as requests for a person to build.
Is the Anvil chain service ready?
Not yet. It is marked experimental, and it has not run end to end with flisk up.
Does the Worker keep the words people type?
Yes, in the demo record. Each write refreshes a 30 day expiry. The words are escaped wherever they are shown.
Can I use the at-sign in the handle?
Yes. A leading at-sign is removed in both the link and the JSON API before the handle is checked.
Why is the reply short?
It is a post on X, so the Worker keeps it under 280 characters with the link. Longer text is shortened before the link, and the link is never cut.
Where is the code?
The Worker is in flisk-sh/flisk-demo-worker, the public demo branches are in flisk-sh/demos, and the Flisk CLI is in flisk-sh/flisk.
Try it#
These five links name words from the menu, so each one produces a stack you can read in full on its own page. The first time you open one, the demo builds. Opening it again returns the same demo.
- demo.flisk.sh/yourhandle/vue-storefront-connected-to-saleor
- demo.flisk.sh/yourhandle/ai-agent-with-mcp-tools
- demo.flisk.sh/yourhandle/python-fastapi-notes-with-redis
- demo.flisk.sh/yourhandle/solidity-dapp-with-postgres-and-email
- demo.flisk.sh/yourhandle/websocket-chat-with-redis-and-cron-jobs
Ask Grok on X with the default example. The short link /try/<words-in-dashes> opens the same post on X with your words filled in, so it can go in a bio or an article.
For the Flisk basics, read the quick start. The command line tool is in github.com/flisk-sh/flisk, the demo branches are in github.com/flisk-sh/demos, and a plain-text summary for models is at flisk.sh/llms.txt.