fline

docs Reference

Reference for AI coding apps

Last checked 17 October 2026

All docs pages This page in Markdown

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://fline.sh/docs/for-ai.md, and an index of every page is at https://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.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, tested end to end, including from the Claude desktop app's Code tab, which reads the same registration:

claude mcp add --transport http --scope user fline https://mcp.fline.sh/mcp

Then /mcp, select fline, choose Authenticate. --scope user registers the server for every folder. Codex is also tested end to end, with the steps on the person's dashboard and at https://fline.sh/docs/connect-codex.md. Setup steps for Cursor, VS Code, Grok and Antigravity are on the dashboard; they are not tested end to end.

Before any tool call works

The person needs all three:

  1. an approved fline account (opened by hand during the beta);
  2. a confirmed email address;
  3. one visit to the dashboard at https://app.dev.fline.sh in 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://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

  1. create_project {name}. Keep project_id, upload_url, blob_key. The answer also carries expires_in (seconds) and max_bytes.
  2. 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.
  3. 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), call get_upload_url {project_id} for a new one.
  4. complete_upload {project_id, blob_key}. Until this call the platform doesn't see the upload, and the project stays in awaiting_source.
  5. Poll get_project. The project moves on its own through analyzing, then possibly parks:
    • awaiting_answers: answer the open questions with answer_questions {project_id, answers: [{key, value}]}.
    • awaiting_secrets: the analysis found variables that need values. If it missed one, add the name with declare_env_names {project_id, names}. Then call get_secrets_link and 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 with code committed_secret, naming a file held back because its name or contents look like a credential. It doesn't hold the project up. 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. If the file is .env.example (or .sample, .template, .defaults), take the key out of it and keep the file.
    • provisioning: the platform is creating or attaching a managed database and writing its connection string. Nothing is asked of you.
    • rendering, then locked: build files are ready.
  6. deploy {project_id} at locked. The worker also queues the build itself after rendering, so deploy on a project already in building, rendering, analyzing, provisioning or submitted returns the current state without error.
  7. Poll get_deploy_status until it settles. desired (what was asked for) and observed (what is running) arrive separately; report observed. The URL is on get_project once the runtime is up.
  8. On failure, fetch the diagnose_a_deployment prompt and follow it: get_project_detail, then get_events, then get_logs.
  9. After the first successful deploy, call get_config and save the result as fline.json at the archive root (see fline.json).

Never ask the person which port the app listens on. No tool takes a port. fline sets PORT=8080 in every container and routes whichever port the process opens. The process must listen on all interfaces (0.0.0.0); a public service that listens only on loopback fails its health check with a reason. See Runtime.

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.json is read only at the archive root.
  • Exclude .git, node_modules and other installed dependencies, build output, and real .env files. Keep .env.example (or .sample, .template, .defaults) with empty or placeholder values: fline reads its variable names, not its values. One of up to 8 KiB gets no warning unless its text holds one of the key patterns below. A larger one isn't read and gets the committed_secret warning like any other .env* file.
  • Size: at most 100 MiB compressed (max_bytes = 104857600). Larger archives are rejected at intake with archive_too_large.
  • At most 20,000 entries, counting files, directories, links and the pax_global_header entry git archive writes first (too_many_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.json is capped at 64 KiB (config_too_large).
  • Refused: absolute paths, paths that escape the root, symlinks pointing outside the archive, and any entry other than a file, directory, link or pax global header, such as a device file or fifo (unsupported_entry_type). git archive --format=tar.gz output is accepted.
  • On macOS, set COPYFILE_DISABLE=1 when running tar so 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* other than an example file of 8 KiB or less, *.pem, id_rsa, id_ed25519, credentials.json, .npmrc, .netrc, *.p12, *.pfx, serviceaccount*.json) and files containing a private key block, sk_live_, AKIA or ghp_ patterns. They are held back from the analysis only: they still go into the build, so leave them out of the archive. Files inside dependency and version-control directories (node_modules, vendor, site-packages, .venv, venv, Pods, .git, .hg, .svn) are neither read nor reported, whatever their names, so a package's own test key is not listed; they still go into the build too. 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:

  • version must be 1. project.id must be this project's id. stack_paths is required; if the stack changed (new package manager or lockfile) the file is refused as written against a different stack: call refresh_config.
  • There is no port field.
  • build_commands run in the build stage in order; run_command becomes the container command. They must agree about paths.
  • Never put a program on run_command (an inline node -e or python -c); the quoting doesn't survive. Commit a script and name the file.
  • No chown in build_commands.
  • A service with image set is prebuilt: nothing is built. Images from docker.io, ghcr.io or quay.io only. Its run_command replaces the image's CMD but not its ENTRYPOINT. Configure it through env_names entries carrying a literal value, for example {"name": "KAFKA_ADVERTISED_LISTENERS", "value": "PLAINTEXT://kafka:9092"}. Services reach each other by service name.
  • A literal value is allowed on any service, built or prebuilt, for non-secret settings such as {"name": "LOG_LEVEL", "value": "info"}. It is refused when the name's last underscore-separated segment is KEY, TOKEN, SECRET, PASSWORD, PASS, PWD, CREDENTIAL, CREDENTIALS or DSN, when the value looks like a credential, or when the entry also carries ref, scope or bind. A literal is stored and shown as written, in records that can't be scrubbed. fline's own analysis writes literals only for prebuilt images, so on a built service a literal comes from your fline.json.
  • volumes lists directories that outlive the container: [{"path": "/data", "reason": "uploads"}], with an absolute path inside the container. See Runtime.
  • Never plan a Postgres, Redis or Valkey service. Declare the variable as managed instead (see Databases).

env_names entries

Each entry is either a bare name or an object. The shape says where the value comes from:

Entry The container gets
"STRIPE_SECRET_KEY" A secret the person types on the private page (see Variables and secrets)
{"name": "DATABASE_URL", "scope": "managed", "ref": "postgres"} The managed database's connection string, written by fline (see Databases)
{"name": "PUBLIC_URL", "scope": "public", "ref": "web"} on service web That service's own public URL, https://<project-id>.apps.dev.fline.sh, or https://<project-id>-<ingress_label>.apps.dev.fline.sh for a labelled service. Always the platform address, never a domain the person attached. The service must be public.
{"name": "API_URL", "scope": "internal", "ref": "api"} http://api:8080: another service of the project, on the project's own network
{"name": "VITE_API_URL", "scope": "public", "ref": "api"} The public URL of another service, for code that runs in the browser. That service must be public.
{"name": "LOG_LEVEL", "value": "info"} The literal string
no entry for PORT or HOST fline writes them itself (see Runtime). A bare PORT or HOST is refused.
  • Write "scope": "public" on a self-reference. A ref naming a service with no scope becomes "internal", so an unscoped self-reference resolves to http://web:8080, which no browser can reach, and nothing refuses it.
  • An internal ref always means port 8080, which reaches a service that listens on PORT. For a prebuilt image on its own port, write a literal instead: {"name": "SEARCH_URL", "value": "http://search:7700"}.
  • bind is "runtime" (the default) or "build". Use "build" for a variable the bundler compiles into browser code, and for every variable of a service whose run_command only serves built files. fline bakes a placeholder into the build and writes the real value into the built files as the container starts, so the value doesn't have to exist at build time. Anything bound to build is readable by whoever loads the page, so never bind a secret to build. bind is refused on a literal and on a managed variable.

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.

Runtime

What every container gets, and what it can't do.

Port and address. fline sets PORT=8080 in every container, and HOST=0.0.0.0 in every service it builds (not in a prebuilt image). An env_names entry under the same name, with a ref or a value, overrides either. Listen on PORT on all interfaces. For a public service fline routes port 8080 if the process listens there, otherwise its only port; with several ports it picks one and says which.

Health. After the containers start, fline waits up to 5 minutes for every service to be healthy:

  • A public service must answer an HTTP GET / on that port. Any status counts, 404 and 500 included; a redirect counts and isn't followed.
  • A service that isn't public is healthy while it runs. If it listens on a port, that port must accept a connection.
  • An image with its own Docker HEALTHCHECK is judged by that instead.

A service that exits during the wait fails the deploy straight away. A public service listening only on 127.0.0.1 or ::1 fails with service listens on loopback only; one with no open port, with service is not listening on any TCP port. The failure names each service that held the deploy back.

Services reaching each other. The services of one project share a private network and reach each other by service name. An internal ref resolves to http://<service>:8080.

Files that last. Declare a volume on the service in fline.json: "volumes": [{"path": "/data", "reason": "uploads"}], with an absolute path. Each volume is a directory kept with the project on fline's machine and mounted at that path. It survives restarts, new versions and rollbacks, and is deleted only with the project. Volumes belong to one service; two services can't share one. All of a project's volumes count toward its 8 GiB of disk and 200,000 files. Anything written outside a volume is lost when the container is replaced, as it is for every new version. No backup of a volume is promised: tell the person to keep their own copy of anything they can't lose.

User. A service fline builds runs as user 10001, not root, and its volume directories belong to that user. It can't install packages or write to system directories while running. A prebuilt image runs as the user its image names.

Outgoing network.

  • Outgoing connections to ports 25, 465, 587 and 2525 are dropped, so the app can't send mail over SMTP. Send email through the email provider's HTTP API, with its key declared as a secret.
  • Private address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 100.64.0.0/10) are unreachable, apart from the project's own services and a managed database wired to it. A database or service on the person's home network or VPN can't be reached.
  • New outgoing connections are limited to 50 a second, shared by every app on the machine. Reuse connections rather than opening one per request.

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: false with a name whose last underscore-separated segment is KEY, TOKEN, SECRET, PASSWORD, PASS, PWD, CREDENTIAL, CREDENTIALS or DSN. Rename it or send secret: true.
  • An empty value. Declare the name and let the person fill it in.
  • secret: false on a name that already holds a secret. Call delete_env_name first.
  • 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_service is separate. It makes an account-level database that the plan path uses only if it is named <project-name>-postgres or <project-name>-valkey. Otherwise you get two. Call list_services before creating one. A database made with create_service and 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.
  • format on 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 a rediss:// URL; an app reading a bare REDIS_ADDR host:port must be changed to read a URL.

What a managed database is:

  • create_service {name, engine, version?, size_class?}: engine is postgres (PostgreSQL 16) or valkey (Valkey 8, Redis-compatible). One size: 1 GiB memory, about 8 GiB of data. It starts provisioning and becomes active once its certificate is issued; poll get_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. rollback never 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

  1. 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.
  2. list_domains shows each domain's verification state, the TXT record, last check and last objection, and the platform hostname to point at.
  3. 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): one CNAME from 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 TXT record at _smartline-challenge.<hostname> holding the token. The _smartline prefix 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 www address.
  • 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_domain cannot 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), then rollback {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:

  1. 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.
  2. get_events: the state timeline with reasons and which component moved it.
  3. 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 with service. Read it while the symptom is present. The dashboard searches 7 days of runtime output; you get only recent lines.
  4. list_env_names: a required, unset variable holds the project at the secrets gate.
  5. get_images: which build is actually running.

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; service listens on loopback only is an app bound to 127.0.0.1, fixed by listening on 0.0.0.0; mail that never leaves is the blocked SMTP ports (see Runtime); files that vanish after an update were written outside a volume.

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, directories and links
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
Health check 5 minutes after start
Volumes inside the project's 8 GiB and 200,000 files
Outgoing mail ports 25, 465, 587 and 2525 blocked; use an HTTP API
New outgoing connections 50 a second, shared by every app on the machine
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://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, unsupported_entry_type 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

The same procedures are published as Agent Skills, which an AI app loads by itself when a task matches, with no command from the person. For Claude Code they come as a plugin that also connects this server. Install steps: Add fline's skills to your AI app. Source: https://github.com/smartline-ai/fline-skills.

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://fline.sh/llms.txt and https://fline.sh/docs/for-ai.md before changing how it is built or deployed.

- fline is connected as an MCP server at https://mcp.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.
- The app can't send mail over SMTP on fline: send email through the provider's HTTP API. Files that must outlast an update go in a volume declared in fline.json.
- To update: get_upload_url, upload the tar.gz, complete_upload, then follow get_project.
- To undo a bad update: list_versions, then rollback.
- fline's step-by-step skills for AI apps, and how to add them: https://fline.sh/docs/skills.md

Next: When something goes wrong or Start here