fline

docs Reference

Reference for AI coding apps

Last checked 16 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://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/mcp

Then /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:

  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://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

  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 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, 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 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.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): 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 with archive_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.json is 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=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*, *.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. 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"}. A literal is allowed only on a service that names an image, is never a credential, and carries no ref, scope or bind. 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: 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.

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