docs Reference
Reference for AI coding apps
Last checked 16 October 2026
This page is written for AI coding apps (Claude Code, Cursor, Codex and others) that are helping a person put a project online with fline. It is precise rather than friendly. If you are the person, you don't need it: ask Claude Code, and point it here. The same content is plain Markdown at https://dev.fline.sh/docs/for-ai.md, and an index of every page is at https://dev.fline.sh/llms.txt.
Where this page and a tool's live answer disagree, the tool wins; tell the person if you notice a disagreement.
What fline is
fline is hosting for apps people build with an AI coding app on their own computer. You pack the project folder as a gzipped tar, upload it to a signed URL, and fline analyses the source, writes its own build files, builds images, runs the containers and gives the app an HTTPS URL of the form <project-id>.apps.dev.fline.sh. It can also create a managed database and serve the app at a domain the person owns.
- Private beta. Accounts are opened by hand. Room on the platform is small (see Capacity).
- One rented machine. No failover, no uptime commitment, no backup or restore promise.
- One person per account. No teams.
- To plan builds, fline sends parts of the source to OpenAI's GPT-5. Saved secrets never reach it. A key written into source files can, apart from files that are held back (see Archive rules). Tell the person this if they ask who reads their code.
Connecting
| Item | Value |
|---|---|
| MCP endpoint | https://mcp.dev.fline.sh/mcp (Streamable HTTP) |
| Protocol revisions | 2026-07-28 (via server/discover), 2025-06-18, 2025-03-26 |
| Server capabilities | tools and prompts. No resources. |
| Authorization | OAuth 2.1 authorization code with PKCE, through Ory Hydra. Dynamic client registration is open. |
| Protected resource metadata | /.well-known/oauth-protected-resource on the MCP host |
| Authorization server metadata | /.well-known/oauth-authorization-server on the MCP host (Hydra's metadata with fline's registration_endpoint and scopes merged in) |
| Scopes | project:read, project:write, deploy, domain:write, offline_access. Registration also grants openid by default. |
| Token lifetime | access token 15 minutes, refresh token 30 days |
| Unauthenticated | initialize, tools/list, prompts/list, prompts/get |
| Authenticated | every tools/call |
No scope reads a secret value. offline_access only obtains a refresh token.
Claude Code, the client tested end to end:
claude mcp add --transport http fline https://mcp.dev.fline.sh/mcpThen /mcp, select fline, choose Authenticate. Without -s user the server is registered for the current folder only. Setup steps for Cursor, Codex, VS Code, Grok and Antigravity are on the person's dashboard; they are not tested end to end.
Before any tool call works
The person needs all three:
- an approved fline account (opened by hand during the beta);
- a confirmed email address;
- one visit to the dashboard at
https://app.dev.fline.shin a browser, which creates their workspace.
Until then OAuth completes normally, and every tools/call returns HTTP 401 with WWW-Authenticate: Bearer realm="fline", resource_metadata="...", error="invalid_token" and one of these messages:
| Message begins | Meaning | What to do |
|---|---|---|
this account is waiting for approval |
Account not yet approved. The token is valid. | Tell the person their account isn't open yet and they'll get an email when it is. Do not re-run OAuth; it changes nothing. |
this account has never asked for access |
No access row: they never loaded the dashboard, or their address is unconfirmed. | Ask them to sign in at https://app.dev.fline.sh once and confirm their address with the emailed code. |
token owner is not provisioned |
Approved, but the workspace isn't created. | Ask them to open https://app.dev.fline.sh once in a browser. |
this account has not been granted access |
Denied. | Tell them; they can use the contact links on https://dev.fline.sh/#contact. |
Your client may present this 401 as "needs authentication" and offer to sign in again. If the message above is visible to you, relay it to the person in plain words instead of looping.
Tools
Write tools take an optional idempotency_key; reuse the same key to retry a call safely.
| Scope | Tools |
|---|---|
project:read |
list_projects, get_project, get_project_detail, get_deploy_status, get_events, get_logs, get_images, get_usage, list_versions, list_env_names, list_domains, list_services, get_service, list_external, get_config, check_config |
project:write |
create_project, delete_project, get_upload_url, complete_upload, submit_declaration, answer_questions, declare_env_names, get_secrets_link, set_env, delete_env_name, create_service, delete_service, pause_service, resume_service, attach_external, detach_external, refresh_config |
deploy |
deploy, restart, rollback, stop |
domain:write |
attach_domain, check_domain, delete_domain |
Annotated destructive: delete_project, delete_service, delete_domain, delete_env_name, set_env (it replaces an existing value), pause_service, detach_external, stop. Ask the person before any of them.
stop is not implemented and always refuses. A project cannot be stopped or paused, only deleted (or have its domain removed with delete_domain).
First deploy: call order
create_project{name}. Keepproject_id,upload_url,blob_key. The answer also carriesexpires_in(seconds) andmax_bytes.- Optional:
submit_declaration{project_id, declaration}with a short object describing the project. It is read by the analysis as an untrusted hint and cannot change platform rules. Send it before step 4 if you send it at all. - Build the archive and PUT it to
upload_url(see Archive rules). The PUT goes straight to object storage; no extra headers are signed. Example:curl -sS -T project.tar.gz "<upload_url>". If the link has expired (15 minutes), callget_upload_url{project_id}for a new one. complete_upload{project_id, blob_key}. Until this call the platform doesn't see the upload, and the project stays inawaiting_source.- Poll
get_project. The project moves on its own throughanalyzing, then possibly parks:awaiting_answers: answer the open questions withanswer_questions{project_id, answers: [{key, value}]}.awaiting_secrets: the analysis found variables that need values. If it missed one, add the name withdeclare_env_names{project_id, names}. Then callget_secrets_linkand hand the URL to the person unopened. Tell them: open it in a browser, sign in to fline if asked, type each value, save, then come back and say done. It works once, for 10 minutes. Never ask them to paste a secret into the chat.- a warning listing a quarantined file that holds a credential: tell the person which file. If it is a live key, suggest they get a new one from the service that issued it. The file was held back from the analysis only and still went into the build, so re-upload without it.
provisioning: the platform is creating or attaching a managed database and writing its connection string. Nothing is asked of you.rendering, thenlocked: build files are ready.
deploy{project_id}atlocked. The worker also queues the build itself after rendering, sodeployon a project already inbuilding,rendering,analyzing,provisioningorsubmittedreturns the current state without error.- Poll
get_deploy_statusuntil it settles.desired(what was asked for) andobserved(what is running) arrive separately; reportobserved. The URL is onget_projectonce the runtime is up. - On failure, fetch the
diagnose_a_deploymentprompt and follow it:get_project_detail, thenget_events, thenget_logs. - After the first successful deploy, call
get_configand save the result asfline.jsonat the archive root (see fline.json).
Never ask the person which port the app listens on. No tool takes a port. fline passes PORT into every container and routes whichever port the process opens. The process must listen on all interfaces (0.0.0.0); an app that listens only on loopback is refused with a reason.
Never write a Dockerfile for fline. fline generates one per built service from the plan; a Dockerfile in the project is read as evidence only.
Archive rules
- Format: gzip-compressed tar. No zip, no Git URL, no inline upload.
- Create it from inside the project folder so paths are relative to the project root, for example
tar -czf ../project.tar.gz -C /path/to/project --exclude=.git --exclude=node_modules ..fline.jsonis read only at the archive root. - Exclude
.git,node_modulesand other installed dependencies, build output, and real.envfiles. Keep.env.example(or.sample,.template,.defaults): fline reads its variable names, not its values. It is still listed among quarantined files by name. - Size: at most 100 MiB compressed (
max_bytes= 104857600). Larger archives are rejected at intake witharchive_too_large. - At most 20,000 regular files; uncompressed-to-compressed ratio at most 100:1. A source, entrypoint or build-config file larger than 32 MiB is refused (
file_too_large), so keep large data files out.fline.jsonis capped at 64 KiB (config_too_large). - Refused: absolute paths, paths that escape the root, symlinks pointing outside the archive.
- On macOS, set
COPYFILE_DISABLE=1when runningtarso it doesn't add._*metadata files. fline does not filter them; they are counted as files. - Held back from analysis (quarantined and reported by name): files named like credentials (
.env*,*.pem,id_rsa,id_ed25519,credentials.json,.npmrc,.netrc,*.p12,*.pfx,serviceaccount*.json) and files containing a private key block,sk_live_,AKIAorghp_patterns. They are held back from the analysis only: they still go into the build, so leave them out of the archive. This is a backstop, not a scanner; most keys in source would get through.
fline.json
get_config returns the plan as a fline.json document. Saved at the archive root, later deploys build from it with no re-analysis of the plan (a model call still screens each upload, and build files are still generated each deploy). smartline.json is accepted as a legacy name; if both are present, fline.json wins.
Decoding is strict: unknown fields are refused. Use notes for comments.
{
"version": 1,
"project": { "id": "<project id>", "platform": "<platform>" },
"declaration": {},
"answers": { "<question key>": "<answer>" },
"notes": "free text",
"stack_paths": ["package.json", "package-lock.json"],
"services": [
{
"name": "web",
"context": ".",
"image": "",
"language": "node",
"evidence": "why the analysis chose this",
"base_image": "node:22",
"runtime_image": "node:22-slim",
"build_commands": ["npm ci", "npm run build"],
"run_command": ["node", "server.js"],
"public": true,
"ingress_label": "",
"depends_on": [],
"env_names": [
"STRIPE_SECRET_KEY",
{ "name": "DATABASE_URL", "scope": "managed", "ref": "postgres" }
],
"volumes": [{ "path": "/data", "reason": "uploads" }],
"notes": ""
}
],
"domains": ["www.example.com"]
}
Rules the checker enforces, and the mistakes they prevent:
versionmust be 1.project.idmust be this project's id.stack_pathsis required; if the stack changed (new package manager or lockfile) the file is refused as written against a different stack: callrefresh_config.- There is no port field.
build_commandsrun in the build stage in order;run_commandbecomes the container command. They must agree about paths.- Never put a program on
run_command(an inlinenode -eorpython -c); the quoting doesn't survive. Commit a script and name the file. - No
chowninbuild_commands. - A service with
imageset is prebuilt: nothing is built. Images fromdocker.io,ghcr.ioorquay.ioonly. Itsrun_commandreplaces the image's CMD but not its ENTRYPOINT. Configure it throughenv_namesentries carrying a literalvalue, for example{"name": "KAFKA_ADVERTISED_LISTENERS", "value": "PLAINTEXT://kafka:9092"}. A literal is allowed only on a service that names an image, is never a credential, and carries noref,scopeorbind. Services reach each other by service name. - Never plan a Postgres, Redis or Valkey service. Declare the variable as managed instead (see Databases).
Loop: edit the file, call check_config {project_id, config} (free, checks against the last uploaded archive) until it answers ok, then upload. refresh_config re-runs the analysis ignoring the file, costs an analysis and is asynchronous: poll get_project until it leaves analyzing, then get_config.
Variables and secrets
The rule: never ask the person to paste a secret into the chat. Declare the name and hand them the link.
| Tool | Does |
|---|---|
declare_env_names |
Declares names only. Values are not accepted. |
get_secrets_link |
Returns {url, expires_at, instruction}: a one-use link to a form, valid 10 minutes, that only the signed-in, approved owner can use. It shows each declared name that has no value yet. Do not open, follow or relay its contents. |
set_env {project_id, name, value, secret} |
secret is required. secret: true writes to the secret store; nothing can read it back, this tool included. secret: false writes a plaintext setting that list_env_names and the dashboard show. |
list_env_names |
Names, whether each is set, required, declared, and the value for plaintext settings only. Never a secret value. |
delete_env_name |
Removes a value. A name the analysis declared stays required, so the next deploy waits at the secrets gate. |
restart |
Re-applies the current version. The only way a secret written after the last deploy reaches running containers. |
What set_env with secret: true is for: storing a value you already hold. That is one you generated (a random session key), or one the person already pasted into the chat before you could stop them. Never ask for a key in the chat to use this path; get_secrets_link is the way in for anything the person has. It is honest to say "fline won't show it again"; it is not honest to say "your AI never saw it". For a key that went through the chat, suggest the person get a fresh one from its issuer and type it on the get_secrets_link form.
set_env refusals:
secret: falsewith a name whose last underscore-separated segment isKEY,TOKEN,SECRET,PASSWORD,PASS,PWD,CREDENTIAL,CREDENTIALSorDSN. Rename it or sendsecret: true.- An empty value. Declare the name and let the person fill it in.
secret: falseon a name that already holds a secret. Calldelete_env_namefirst.- A name the plan binds to a managed database or addon. The platform writes that value itself.
secret: true on a name that already holds a secret replaces it. Either kind takes effect on the next deploy or restart. A plaintext setting survives re-analysis; a secret added by hand under a name the analysis didn't declare does not.
The person can also set or replace a secret themselves on the project's Variables page in the dashboard. That never passes through you.
If the app prints a saved secret verbatim, fline blanks exact matches longer than 2 characters out of the logs you read. Partial or transformed copies get through.
Databases
The truth, from the worker: when the plan marks a variable as scope: "managed" with ref "postgres" or "valkey", the platform wires it. After analysis the project moves to provisioning; the worker reuses the database already bound to this project, or else the account's oldest database named <project-name>-<engine> (lowercased, at most 64 characters), or else creates one with that name. It waits until the database is active and writes the connection string into the project's secrets under the planned variable name. Nobody is asked for it, and you must not declare_env_names it or set_env it (set_env refuses).
Consequences:
- Prefer the plan path. If the analysis didn't mark the variable managed, fix it in
fline.json({"name": "DATABASE_URL", "scope": "managed", "ref": "postgres"}),check_config, and re-upload. create_serviceis separate. It makes an account-level database that the plan path uses only if it is named<project-name>-postgresor<project-name>-valkey. Otherwise you get two. Calllist_servicesbefore creating one. A database made withcreate_serviceand not wired by the plan needs its connection string copied by the person from the dashboard's Services page into the secrets form. No tool returns a connection string or password.- A paused database blocks wiring. Provisioning fails with a message asking the person to resume it.
formaton a managed variable: empty (the URL, the default),"libpq"for keyword/value Postgres configuration,"host_port"for a bare host:port. Valkey is TLS only and produces arediss://URL; an app reading a bareREDIS_ADDRhost:port must be changed to read a URL.
What a managed database is:
create_service{name, engine, version?, size_class?}:engineispostgres(PostgreSQL 16) orvalkey(Valkey 8, Redis-compatible). One size: 1 GiB memory, about 8 GiB of data. It startsprovisioningand becomesactiveonce its certificate is issued; pollget_service.- It belongs to the account, outlives any project, and can serve several projects.
- Reachable from the internet over TLS with its password (Postgres DSN carries
sslmode=verify-full). The person can restrict source addresses on the dashboard ("Who may reach it"). pause_service/resume_service: reversible; paused refuses connections, keeps data.delete_service: stops connections; data kept seven days, then destroyed. Ask first.- No backups are promised and there is no restore or download. Tell the person to keep their own copy of anything they can't lose.
rollbacknever changes database contents.
Bring your own: attach_external {project_id, offering, alias?, variable?} with offering one of postgres, redis, mysql, mongodb, rabbitmq. It takes the shape only; variable defaults to DATABASE_URL, REDIS_URL, MONGODB_URI or AMQP_URL. The project moves to awaiting_secrets and the person types the URL on the get_secrets_link form. detach_external undoes it; list_external lists them.
MySQL is not a managed offering. A project that needs one either attaches an external MySQL or runs it as an ordinary prebuilt-image service.
Domains
attach_domain{project_id, domain}returns the record to add, once. You cannot add it for the person; they add it at their DNS provider.list_domainsshows each domain's verification state, the TXT record, last check and last objection, and the platform hostname to point at.check_domain{project_id, hostname}runs a check now. It is rate-limited behind a certificate budget: poll it slowly, never in a tight loop. Do not call it on a failed domain: it answers an internal error that reads like an outage.
Records:
- A subdomain (
www,app): oneCNAMEfrom the hostname to the project's platform hostname. It proves ownership and routes traffic. Works at any provider. - Ownership can also be proven with a
TXTrecord at_smartline-challenge.<hostname>holding the token. The_smartlineprefix is an older name; use it exactly as given. A TXT alone proves ownership but routes nothing. - An apex (bare) domain: confirmed only on Cloudflare (CNAME flattening). On other providers, use a
wwwaddress. - Cloudflare: the record must be "DNS only" (not proxied).
- After 48 hours a domain that still isn't proven is marked failed, and it never leaves that state: remove it and add it again (
delete_domain, fix the record,attach_domain).check_domaincannot revive it. - Once verified, fline obtains the certificate and renews it.
The dashboard's Domains page shows the record in the person's provider's own field names (Cloudflare, GoDaddy, Namecheap, Squarespace Domains, AWS Route 53, other). Send the person there rather than dictating field names.
Updates and rollback
- Update:
get_upload_url, PUT the new archive,complete_upload. On a project already running, the old runtime keeps running while the new version is analysed and built. Then follow the same polling as a first deploy. A failure before submit leaves the old version running. - Rollback:
list_versions{project_id, limit?}(newest first:state_version, when, who, what it superseded), thenrollback{project_id, to}. It deploys the old version's contents as a new, higher version (rolling back to 3 can put the project on 12). It restores the specification, not the secrets: current values are re-pinned. It is refused if the stored version no longer validates. There is no rollback button in the dashboard. - Domains, managed databases and secrets persist across updates and rollbacks.
Diagnosis
Fetch the diagnose_a_deployment prompt. In short:
get_project_detail: per-container state, health, restart count, image digest, last failure with its class. A climbing restart count is a crash loop in the app.get_events: the state timeline with reasons and which component moved it.get_logs{project_id, kind, lines?, service?}:kind: "build"is stored build output;kind: "runtime"is a bounded tail of running containers (no history, no search), narrowed withservice. Read it while the symptom is present. The dashboard searches 7 days of runtime output; you get only recent lines.list_env_names: a required, unset variable holds the project at the secrets gate.get_images: which build is actually running.
Two lookalikes: a secret entered after the last deploy isn't in running containers until restart; a prebuilt image ignoring its command is the ENTRYPOINT rule.
The dashboard shows failures with a button that copies the failure text for the person to paste to you.
Limits and capacity
| Limit | Value |
|---|---|
| Project size class | 1 vCPU, 1 GiB memory, 8 GiB disk, shared by all services. Not changeable. get_usage reports consumption, not charges. |
| Managed database | 1 GiB memory, about 8 GiB data |
| Archive | 100 MiB compressed, 20,000 files |
| Upload link | 15 minutes |
| Secrets link | one use, 10 minutes |
| Build | each attempt stops at 15 minutes |
| Runtime output | 7 days searchable in the dashboard; tools get a recent tail |
| Usage history | up to 30 days (get_usage windows 1h, 6h, 24h, 7d, 30d) |
| Domain verification | 48 hours |
| Scheduled jobs | none (no cron field) |
Capacity: the platform holds a small number of projects across all users. There is no queue. When it is full, a new project goes through upload, analysis and build and is then refused at hand-off: the project fails at stage submit with a reason beginning no_capacity. Do not retry. Tell the person fline is full right now and suggest they write to the founder via https://dev.fline.sh/#contact. Existing projects can still update and roll back.
Error reference
| Where | Code or text | Meaning | Do |
|---|---|---|---|
any tools/call |
HTTP 401, invalid_token |
Account pending, unconfirmed, not provisioned, or denied (see Before any tool call works) | Relay the message; don't loop on OAuth |
any tools/call |
HTTP 401, no error |
No token | Start OAuth |
any tools/call |
insufficient_scope |
Token lacks the tool's scope | Re-authorize with that scope |
| state tools | the project is in state X; ... |
Wrong state for this call | Follow the sentence after the semicolon |
get_project failure, stage intake |
archive_too_large, archive_empty, too_many_files, compression_bomb, file_too_large, config_too_large, archive_unreadable, absolute_path, path_traversal, symlink_escape, bad_path |
Archive refused | Rebuild the archive per Archive rules |
stage analyze |
reason text | Analysis failed | Read the reason; consider writing fline.json |
stage provision |
e.g. managed service paused | Managed database couldn't be wired | Ask the person to resume it in the dashboard, then upload again |
stage render / build |
reason text, build logs | Build files or build failed | get_logs kind=build; fix build_commands/run_command |
stage submit |
no_capacity: ... |
Platform full | Stop; tell the person |
| deploy failure class | invalid_manifest, image_pull_failed, healthcheck_failed, secrets_unavailable, converge_timeout, internal_error |
Runtime hand-off failed | get_project_detail, get_events; internal_error is the platform's, tell the person |
set_env |
refusal sentence | See Variables and secrets | Follow the sentence |
stop |
not implemented | No stop exists | delete_domain or delete_project |
| secrets form | expired or used | One-use, 10 minutes | Call get_secrets_link again |
| upload PUT | expired | Link older than 15 minutes | get_upload_url |
To retry a failed project, upload again (get_upload_url, PUT, complete_upload). deploy does not restart a failed project.
Prompts
Fetched with prompts/get, no authentication.
| Name | Argument | Content |
|---|---|---|
deploy_a_repository |
name (optional) |
Call order from create_project to a running URL |
update_a_live_app |
project_id (required) |
A new version, a changed setting, or an earlier version put back |
add_a_database |
project_id (optional) |
A managed Postgres or Valkey wired through the plan, or a database the person already runs |
use_a_web_address |
project_id (required), domain (optional) |
The record the person adds, and how fline checks it |
diagnose_a_deployment |
project_id (required) |
What to read, in order, when a deploy fails or isn't answering |
freeze_the_build_plan |
project_id (required) |
Committing fline.json and keeping it correct |
explain_to_the_person |
none | How to talk to someone who is not a programmer, and what never to ask them |
Talking to the person
The person is often not a programmer. They built the app with you and want it online. Use their words, not ours:
| Instead of | Say |
|---|---|
| deploy | put it online |
| repository, repo | your project folder |
| environment variables, secrets | passwords and keys your app uses; settings, for non-secret values |
| secrets link, form | a private fline page where you type it |
| console | your fline dashboard |
| logs | what went wrong; what your app printed |
| rollback | put an earlier version back |
| DNS record, CNAME | one record at the company where you bought your web address |
| TLS certificate | the padlock |
| connection string | the database's address line |
| Valkey, Redis | a fast cache |
| OAuth, scopes | approve fline in your browser |
| build failed at render | it stopped while getting your app ready |
Rules that matter to them:
- Before uploading, say you're about to pack and send a copy of their project folder to fline. The original stays on their computer.
- Never ask them to paste a key into the chat. Give them the private page and say: it works once, for 10 minutes, only while signed in; you'll see that they saved it, not what they typed.
- Never promise backups, uptime, timings, a free tier, or that a fix will work.
- Their app's link is public. If the app holds personal data, offer to check that each user sees only their own data.
- If something needs them (a record at their domain company, a value on the private page, resuming a database, a dashboard visit), say exactly what and where, then wait.
- fline is free during the private beta, with no card. Their AI coding app's own plan is separate and is used as usual when you work for them.
A snippet for the project's AGENTS.md or CLAUDE.md
The person can drop this into their project so any AI coding app working in the folder knows how the project goes online:
## Hosting
This project is put online with fline. Read https://dev.fline.sh/llms.txt and https://dev.fline.sh/docs/for-ai.md before changing how it is built or deployed.
- fline is connected as an MCP server at https://mcp.dev.fline.sh/mcp. Use its tools; don't write a Dockerfile or pick a port.
- fline.json at the project root is fline's build plan. Check edits with check_config before uploading.
- Never ask me to paste a password or key into the chat. Use get_secrets_link and give me the link unopened.
- To update: get_upload_url, upload the tar.gz, complete_upload, then follow get_project.
- To undo a bad update: list_versions, then rollback.Next: When something goes wrong or Start here