# SeeDropHost — full documentation > All documentation pages of https://seedrophost.com in one file, as Markdown. Checked against the code on 2026-10-31 (version 0.1.0). # Quickstart Checked against the code on 2026-10-30 (version 0.1.0). SeeDropHost publishes sites that are already built: one HTML file, a `.zip` or a folder. There is no public hosted instance yet, so the first step is to run a server — on your computer (below) or on a VPS ([self-host guide](/docs/self-host/)). > **Note:** billing for hosted plans opens with the first public server ([pricing](/pricing/)). Everything below runs on your own machine at no cost from us. ## 1. Run the server Requires Node.js 24 (it runs TypeScript directly, no build step). ```bash git clone seedrophost && cd seedrophost npm install npm run dev ``` Development mode (`SDH_DEV=1`) uses the token `dev-token` and serves: - dashboard and API: `http://localhost:8787` - sites: `http://.localhost:8787` In the commands below, set: ```bash export SDH_API_URL=http://localhost:8787 export SDH_TOKEN=dev-token ``` ## 2. Create a site ```bash curl -X POST "$SDH_API_URL/v1/sites" \ -H "Authorization: Bearer $SDH_TOKEN" -H "content-type: application/json" \ -d '{"name":"my-site"}' ``` Or `npx sdh create my-site`. Names: lowercase letters, digits and hyphens, up to 63 characters, no `--`. ## 3. Publish — pick one of three ways ### A single HTML file ```bash curl --data-binary @index.html -H "content-type: text/html" \ -H "Authorization: Bearer $SDH_TOKEN" \ "$SDH_API_URL/v1/sites/my-site/deploys/html" ``` The file becomes `/index.html` of the site. ### A .zip ```bash curl --data-binary @site.zip -H "content-type: application/zip" \ -H "Authorization: Bearer $SDH_TOKEN" \ "$SDH_API_URL/v1/sites/my-site/deploys/zip" ``` If everything in the zip sits inside one top-level folder, that folder becomes the root of the site. ### A folder (CLI) ```bash npx sdh deploy ./dist --site my-site ``` Expected output (numbers vary): ```text ✓ deploy dpl-… pronto em 0.4s 12 arquivos, 12 enviados (o resto já estava guardado) prévia: http://dpl-…--my-site.localhost:8787 no ar: http://my-site.localhost:8787 SEO/GEO: nota 91/100, 3 ponto(s) a melhorar ``` The CLI and API messages are in Brazilian Portuguese today. Run the same command again: `0 enviados` — only changed files upload. ## 4. Read the result The API answer (and `npx sdh deploys my-site`) includes: - `urls.site`: the live address; `urls.deploy`: the address of this exact deploy. - `deploy.report.seo.score` (0–100) and `issues`: what to fix for search engines and AI answers. - `deploy.report.forms`: forms found in the HTML. - `deploy.report.rules.problems`: lines of `_redirects` / `_headers` that were not understood. ## 5. Draft first, then publish Add `?draft=1` (API) or `--draft` (CLI). Nothing changes for visitors; you get a preview URL served with `noindex`. ```bash npx sdh deploy ./dist --site my-site --draft npx sdh publish my-site dpl-… ``` ## Undo Every deploy stays available. To roll back, publish an earlier one: ```bash npx sdh deploys my-site # find the previous deploy id npx sdh publish my-site dpl-previous ``` ## Without an account (if the server allows it) When the operator starts the server with `SDH_ANON_DROP=1`, anyone can publish without a token. The site lives for 24 hours unless it is claimed. ```bash curl --data-binary @site.zip -H "content-type: application/zip" "$SDH_API_URL/v1/drop" # or: npx sdh drop ./dist ``` The answer has `url`, `expires_at` and a `claim_token` that is shown only once. Claim it with a token: `npx sdh reivindicar --nome my-site`. Anonymous sites are never indexed and do not accept form submissions. See [Limits](/docs/limits/). ## Next - [HTTP API](/docs/api/) · [CLI](/docs/cli/) · [MCP for agents](/docs/mcp/) - [Migrate from Netlify](/docs/migrate-from-netlify/) · [Self-host on a VPS](/docs/self-host/) --- # HTTP API Checked against the code on 2026-10-31 (version 0.1.0). Base URL: your server, for example `http://localhost:8787` in development. Machine-readable spec: [/openapi.json](/openapi.json) (OpenAPI 3.1). ## Authentication Every route needs `Authorization: Bearer `, except `GET /v1/health`, `POST /v1/drop` and the Asaas webhook (see [Billing](#billing)). There are two kinds of token: - **Account token** (`sdh_…`): created with `POST /v1/account/tokens`, up to 20 active per account. It sees and changes only the sites of its own account. A site, deploy or token of another account answers `404`, the same as one that does not exist. - **Server token** (`SDH_TOKEN`): the administrator. It sees every account, is the only one that can create accounts and change plans, and picks the account on account routes with `?conta=` (default: the `principal` account, where sites created before accounts live). These are tokens with labels, not people: there are no user identities or roles yet. Optional header `x-sdh-actor: ` (up to 64 characters) sets who appears in the audit log; the default is `api`. Errors look like this (messages are in Brazilian Portuguese today). The ones you will meet most: | Status | `error.code` | When | |---|---|---| | `401` | `unauthorized` | token missing, invalid or revoked | | `402` | `plan_limit` | creating (or claiming) a site beyond the plan's site count; `error.detail` has `plano` and `limite`. Nothing that is live is touched | | `403` | `admin_only` | the route is for the server administrator | | `404` | `not_found` and friends | does not exist, or belongs to another account | | `409` | `token_limit`, `anonymous_site` | 20 active tokens already; export of an anonymous site | ```json { "error": { "code": "unauthorized", "message": "token ausente ou inválido" } } ``` ```bash export SDH_API_URL=http://localhost:8787 export SDH_TOKEN=dev-token AUTH="Authorization: Bearer $SDH_TOKEN" # the administrator # account routes below also work with an account token (sdh_…): export ACCOUNT_TOKEN=sdh_… ``` ## Health ### GET /v1/health No token. Version and a database check; `503` if the database does not answer. ```bash curl "$SDH_API_URL/v1/health" # {"ok":true,"service":"seedrophost","version":"0.1.0","db":"ok"} ``` ## Sites ### GET /v1/sites An account token lists only its own sites. The administrator sees all of them, or one account with `?conta=`. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites" ``` ### POST /v1/sites Body `{"name": "..."}`. `201` with the site, created in the account of the token (the administrator may add `"conta": ""`; default `principal`). Beyond the plan's site count the answer is `402 plan_limit`. Reserved names: `www`, `api`, `app`, `admin`, `edge`, `mcp`, `status`, `docs`, `mail`, and anything starting with `site-`, `dpl-`, `acc-` or `tok-` (they look like internal ids). ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" -d '{"name":"my-site"}' "$SDH_API_URL/v1/sites" ``` ### GET /v1/sites/{site} `{site}` is the name or the id. Adds `url` and `domains`. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site" ``` ### DELETE /v1/sites/{site} Takes the site down at once and deletes it with its deploys, domains, forms and leads; files no other deploy uses are removed from storage. Answer `200`: `{"deleted": "my-site"}`. Recorded in the audit log as `site.delete`. There is no undo. ```bash curl -X DELETE -H "$AUTH" "$SDH_API_URL/v1/sites/my-site" ``` ## Deploys in one step The request body is the file itself. A zip of a repository root with `netlify.toml` / `seedrop.toml` is laid out like the CLI does: only the `publish` folder goes live, with the config and functions kept private. Query options: `draft=1` (do not publish), `title=` (up to 200 characters), `source=` (up to 32 characters). Answer `201`: `{deploy, urls, files, uploaded, bytes}`, where `deploy.report` is the SEO/GEO report. ### POST /v1/sites/{site}/deploys/zip ```bash curl --data-binary @site.zip -H "content-type: application/zip" -H "$AUTH" \ "$SDH_API_URL/v1/sites/my-site/deploys/zip?title=first" ``` ### POST /v1/sites/{site}/deploys/html The body becomes `/index.html`. ```bash curl --data-binary @index.html -H "content-type: text/html" -H "$AUTH" \ "$SDH_API_URL/v1/sites/my-site/deploys/html?draft=1" ``` ## Deploys by manifest (only changed files upload) This is what the CLI uses for folders. ### POST /v1/sites/{site}/deploys Body: `files` maps each public path to the SHA-256 (hex) of its content; optional `draft`, `title`, `source`, `git_ref`. Answer `201`: `{deploy, required, urls}`. `required` lists the hashes the server does not have yet; when it is empty, the deploy is already `ready`. ```bash H=$(shasum -a 256 index.html | cut -d' ' -f1) curl -X POST -H "$AUTH" -H "content-type: application/json" \ -d "{\"files\":{\"/index.html\":\"$H\"},\"draft\":true,\"title\":\"manifest\"}" \ "$SDH_API_URL/v1/sites/my-site/deploys" ``` ### PUT /v1/deploys/{id}/blobs/{hash} Upload one missing file; the server checks that the content matches the hash. ```bash curl -X PUT --data-binary @index.html -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…/blobs/$H" # {"ok":true} ``` ### POST /v1/deploys/{id}/finalize Closes the deploy after the uploads, builds the report and, if it is not a draft, publishes it. `409 missing_files` lists what is still missing. ```bash curl -X POST -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…/finalize" ``` ### GET /v1/sites/{site}/deploys Latest first. `limit` from 1 to 100 (default 20). Each item has `live`, `url` and `score`. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/deploys?limit=5" ``` ### GET /v1/deploys/{id} The deploy with its full `report` (SEO/GEO, forms, rules). ```bash curl -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…" ``` ### POST /v1/sites/{site}/publish Body `{"deploy_id": "..."}`. Puts a ready deploy live: approves a draft or rolls back. Takes effect on the next request. The `draft` field of a deploy keeps recording how it was created; `live` in the deploy list says what is being served. ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" -H "x-sdh-actor: release-bot" \ -d '{"deploy_id":"dpl-…"}' "$SDH_API_URL/v1/sites/my-site/publish" ``` ## Publishing without an account ### POST /v1/drop No token. Only when the server runs with `SDH_ANON_DROP=1` (otherwise `404`). Body: a zip (`content-type: application/zip`) or an HTML file (`content-type: text/html`), up to 25 MiB; a zip may hold up to 50 MiB uncompressed. Answer `201`: `url`, `expires_at` (24 h), `claim_token` (shown once), `site`, `deploy_id`. Refusals, each with a `Retry-After` header: | Status | `error.code` | When | |---|---|---| | `429` | `rate_limited` | more than 5 per network per hour (IPv4 address or IPv6 `/64`; `Retry-After: 3600`), or the server-wide hourly cap was reached (`Retry-After: 600`) | | `503` | `busy` | 2 drops are already being processed (`Retry-After: 10`) | | `503` | `drop_quota` | storage for drops is full (`Retry-After: 3600`) | | `413` | `too_large` | body above 25 MiB | ```bash curl --data-binary @site.zip -H "content-type: application/zip" "$SDH_API_URL/v1/drop" ``` ### POST /v1/drop/claim With a token. Body `{"claim_token": "...", "name": "optional-new-name"}`. Removes the expiry and the anonymous restrictions, and the site joins the account of the token (and counts toward its site limit: `402 plan_limit` when the plan is full). ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" \ -d '{"claim_token":"ab0e…","name":"my-site"}' "$SDH_API_URL/v1/drop/claim" ``` ## Environment variables Values are encrypted at rest (AES-256-GCM). Secret values are always masked in answers, for example `s3c••••••ue`. ### GET /v1/sites/{site}/env ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/env" ``` ### PUT /v1/sites/{site}/env/{key} Key: `UPPER_CASE`, digits and `_`, up to 128 characters. Body: `value`; optional `secret` (boolean), `scopes` (`build`, `functions`, `runtime`), `contexts` (`production`, `preview`). ```bash curl -X PUT -H "$AUTH" -H "content-type: application/json" \ -d '{"value":"…","secret":true}' "$SDH_API_URL/v1/sites/my-site/env/API_KEY" ``` ### DELETE /v1/sites/{site}/env/{key} ```bash curl -X DELETE -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/env/API_KEY" ``` ## Custom domains A custom domain is only served after its TXT record is verified. ### GET /v1/sites/{site}/domains ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/domains" ``` ### POST /v1/sites/{site}/domains Body `{"hostname": "www.example.com"}`. Answer `201` with the DNS records to create: a TXT `_seedrop.` and a CNAME (or A records for an apex domain). ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" \ -d '{"hostname":"www.example.com"}' "$SDH_API_URL/v1/sites/my-site/domains" ``` ### POST /v1/domains/{hostname}/verify Looks up DNS and answers `verified`, `routed`, what was `found` and the `next` step. ```bash curl -X POST -H "$AUTH" "$SDH_API_URL/v1/domains/www.example.com/verify" ``` ## Forms and leads ### GET /v1/sites/{site}/forms Forms detected in the deploys, with fields, number of submissions, spam blocked and last submission. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/forms" ``` ### GET /v1/sites/{site}/submissions Latest first. Query: `form`, `spam=1` (include spam), `limit` 1–500 (default 50). ```bash curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/submissions?form=contact&limit=20" ``` ## Audit ### GET /v1/audit Latest first, `limit` 1–500 (default 50). Each entry: `actor`, `action`, `target`, `detail_json`, `at`. An account token sees only the entries of its own account; the administrator sees all, or one account with `?conta=`. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/audit?limit=20" ``` ## Accounts (administrator only) These three routes answer `403 admin_only` to an account token. ### POST /v1/accounts Body: `nome` (required, up to 100 characters), optional `plano` (`gratis`, `pro` or `agencia`; default `gratis`) and `rotulo` (label of the first token). Answer `201`: `{conta, token, token_id, aviso}`. The `token` is shown **once**; the server keeps only a SHA-256 of it. ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" \ -d '{"nome":"Client Ltda","plano":"gratis","rotulo":"first token"}' "$SDH_API_URL/v1/accounts" ``` ### GET /v1/accounts Every account with its `sites` count. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/accounts" ``` ### PATCH /v1/accounts/{conta} Body with any of `plano`, `nome`, `pagamento_falhou_em` (ISO date, or `null` when the payment is back). The plan change takes effect at once. With [billing](#billing) on, the server sets `pagamento_falhou_em` by itself when an invoice is overdue and clears it when the invoice is paid. Answers `404` for an unknown account and `400` for an invalid plan or date. ```bash curl -X PATCH -H "$AUTH" -H "content-type: application/json" \ -d '{"plano":"pro"}' "$SDH_API_URL/v1/accounts/acc-…" ``` ## Your account, usage and tokens With an account token these routes act on the token's own account. The administrator adds `?conta=` (default `principal`). ### GET /v1/account The account (`id`, `nome`, `plano`, dates of the warning and plan-change request, `pagamento_falhou_em`), plus `admin` (true when the caller used the server token) and the number of `sites`. ```bash curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account" ``` ### GET /v1/account/usage Plan, limits, usage of the current month and what to do next (texts in Brazilian Portuguese). Sites, bandwidth and leads are counted; **deploys never count**. Going over bandwidth or leads only changes `estado` to `aviso`, and two months in a row to `pedir_troca` (a plan change is requested, with 14 days): no site goes offline and every lead is kept (`leads_acima_do_plano` says how many are above the plan). Other fields: `plano`, `limites`, `historico` (two previous months), `acima_neste_mes`, `meses_seguidos_acima`, `pedir_troca`, `pagamento`, `mensagens`, `proximos_passos`. ```bash curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/usage" # shortened: # {"conta":{"nome":"Client Ltda","plano":"gratis"},"mes":"2026-10", # "limites":{"sites":3,"banda_gb_mes":30,"leads_mes":100,"deploys":"ilimitados"}, # "uso":{"sites":1,"banda_gb":0.05,"leads":4,"leads_acima_do_plano":0,"deploys":6}, # "estado":"ok","proximos_passos":["nada a fazer: a conta está dentro do plano. Deploys são ilimitados."]} ``` ### POST /v1/account/tokens Body: optional `rotulo` (label, up to 100 characters). Answer `201`: `{id, rotulo, criado_em, token, aviso}`. The `token` (`sdh_…`) is shown **once**. Give one token to each machine or agent. At most 20 active tokens per account: the 21st answers `409 token_limit`; revoke one first. ```bash curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" -H "content-type: application/json" \ -d '{"rotulo":"ci"}' "$SDH_API_URL/v1/account/tokens" ``` ### GET /v1/account/tokens The tokens of the account without their secret: `id`, `rotulo`, `criado_em`, `ultimo_uso_em`, `revogado_em`. Revoked tokens stay in the list for 90 days. ```bash curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/tokens" ``` ### DELETE /v1/account/tokens/{token} `{token}` is the token `id` (for example `tok-…`), not the secret. The token stops working at once. A token of another account answers `404 token_not_found`. ```bash curl -X DELETE -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/tokens/tok-…" ``` ## Billing The plan subscription is paid by Pix or boleto on the invoice page of Asaas, the payment provider; a card also works on that page and never passes through the server. Billing is off until the administrator sets `SDH_ASAAS_API_KEY`. The price always comes from the plan table on the server: a value sent by the client is ignored. CPF, CNPJ and email go to Asaas and are not stored on the server. Nothing here takes a site offline or deletes anything: when an invoice is overdue the sites stay live, and only after 30 days does creating a new site follow the Free limits. The hosted plans open with the first public server; there is no date. ### GET /v1/account/billing `estado` (`gratis`, `pendente`, `ativo`, `atrasado` or `cancelado`), `plano`, `ciclo`, `valor`, `pago_ate`, `pagamento_falhou_em`, `proxima_cobranca`, `fatura_em_aberto` (`url`, `vencimento`, `estado` `em_aberto` or `vencida`, `valor`), `troca_pendente` (a change waiting for payment), `troca_agendada` (a move down and when it starts), `precos`, `mensagens` and `proximos_passos`. `cobranca_ligada` is `false` when billing is off on the server. While a payment is awaited, the server reads Asaas again, waiting longer each time (30 s up to 1 h) and for at most 24 h. CLI: `sdh plano`; MCP: `get_billing`. ```bash curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/billing" # shortened: # {"cobranca_ligada":true,"estado":"ativo","plano":"pro","ciclo":"mensal","valor":29, # "pago_ate":"2026-11-30T02:59:59.000Z","proxima_cobranca":{"data":"2026-11-30T02:59:59.000Z","valor":29}, # "fatura_em_aberto":null,"troca_pendente":null,"troca_agendada":null,"mensagens":["plano Pro pago até 29/11/2026."]} ``` ### POST /v1/account/billing/checkout Body: `plano` (`pro` or `agencia`), `ciclo` (`mensal` or `anual`; default `mensal`), `nome` (who pays), `cpfCnpj` (check digits are validated before calling Asaas) and `email`. Send an `Idempotency-Key` header, for example a UUID: repeating it, or asking again for the same plan and cycle, returns the same subscription, and an account never has two open subscriptions. Answer `201`: `plano`, `ciclo`, `valor`, `efeito`, `invoiceUrl`, `pix` (`qr` as a base64 PNG, `copiaecola`, `expira_em`) when the invoice has Pix, `vencimento` and `proximo_passo`. CLI: `sdh assinar pro --nome "…" --cpf … --email …`. `efeito` says when the new plan takes effect: | `efeito` | When | |---|---| | `apos_pagamento` | from Free, or with no paid time left: when the invoice is paid | | `agora` | moving up (a bigger plan, or the same plan in a cycle that costs more per day): at once; the paid time left becomes `credito_dias` days of the new plan, and its first charge is due after them | | `agendada` | moving down: the current plan, already paid, lasts until `pago_ate` (`inicia_em`), then the new one starts. The old subscription stops charging now; nothing is refunded or charged twice | | `retomada` | the same plan again after a cancel: charged again at the end of the paid period | Refusals: `400` (`invalid_plan`, `invalid_cycle`, `invalid_name`, `invalid_cpf_cnpj`, `invalid_email`), `409 already_on_plan`, `409 payment_just_confirmed` (the waiting payment arrived first: check `GET /v1/account/billing` before asking again), `409 scheduled_paid`, `429 checkout_limit` (more than 5 attempts in 24 h) and `503 billing_off`. ```bash curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" -H "content-type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"plano":"pro","ciclo":"mensal","nome":"Client Ltda","cpfCnpj":"11.222.333/0001-81","email":"billing@example.com"}' \ "$SDH_API_URL/v1/account/billing/checkout" ``` ### POST /v1/account/billing/cancel Stops charging now. The plan lasts until `pago_ate` and then the account goes back to Free; nothing is deleted and no site goes offline. Before cancelling, the server reads Asaas again, so a payment that just arrived is never thrown away. Answer `200` with the same body as `GET /v1/account/billing`; `409 no_subscription` when there is nothing to cancel. CLI: `sdh cancelar-assinatura --sim`. ```bash curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/billing/cancel" ``` ### GET /v1/admin/billing Administrator only. What is missing to charge: `chave` (the Asaas key: `ok`, `falta`, `recusada` or `sem_resposta`), `token_webhook`, `host_do_painel`, `webhook` (`cadastrado`, `interrompido`, `desligado` or `falta`), `pronto`, `falta` and `proximo_passo`. Changes nothing. CLI: `sdh cobranca`. ```bash curl -H "$AUTH" "$SDH_API_URL/v1/admin/billing" ``` ### POST /v1/admin/billing/webhook Administrator only. Body: optional `email`, which gets the Asaas warnings when the webhook fails. Registers only the SeeDropHost webhook (`https:///v1/billing/asaas/webhook`) or turns its paused queue back on; other webhooks of the Asaas account are never touched. CLI: `sdh cobranca:webhook --email you@example.com`. ```bash curl -X POST -H "$AUTH" -H "content-type: application/json" \ -d '{"email":"you@example.com"}' "$SDH_API_URL/v1/admin/billing/webhook" ``` ### POST /v1/billing/asaas/webhook Called by Asaas, not by clients. No Bearer: the `asaas-access-token` header must match `SDH_ASAAS_WEBHOOK_TOKEN` (`401` otherwise; `404` when billing is off). Once the token is accepted the answer is always `200`, so Asaas never pauses the queue: events of other systems on the same Asaas account are ignored and each event is processed once. The event is never trusted: the server reads the payment or subscription again on Asaas before changing anything. ## Take everything (export) ### GET /v1/sites/{site}/export A zip with `site/` (the files of the live deploy, including `_redirects`, `_headers` and `netlify.toml` / `seedrop.toml`), `leads.csv` (every submission, with a `spam` column; cells that start with `=`, `+`, `-` or `@` get an apostrophe so they never run as formulas), `variaveis.txt` (the **names** of the variables only: values never leave, not even masked) and `LEIA-ME.txt`. With `?resumo=1` the answer is JSON with the numbers (`arquivos`, `leads`, `variaveis`, `dominios`) and the download path instead of the zip. An anonymous drop answers `409 anonymous_site`: claim it first. ```bash curl -H "$AUTH" -o my-site.zip "$SDH_API_URL/v1/sites/my-site/export" curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/export?resumo=1" ``` ## Internal route ### GET /v1/internal/tls-check Not for clients. Caddy asks it before issuing a certificate (`?domain=`). It only answers calls from the machine itself without proxy headers; anything else gets `403`, and the shipped Caddyfile answers `404` for `/v1/internal/*` from outside. ## MCP endpoint `POST /mcp` speaks MCP (Streamable HTTP, JSON-RPC) with the same Bearer token. See [MCP for agents](/docs/mcp/). --- # CLI: sdh Checked against the code on 2026-10-31 (version 0.1.0). `sdh` is the command line for SeeDropHost. It is not published on npm yet: run it from a clone of the repository with `npx sdh …` or `npm run sdh -- …`. Its messages are in Brazilian Portuguese. ## Configuration | Variable | Default | What it is | |---|---|---| | `SDH_API_URL` | `http://localhost:8787` | your server | | `SDH_TOKEN` | `dev-token` | an account token (`sdh_…`), which only sees its own account's sites, or the server (administrator) token, which sees every account | Every action is recorded in the audit log with the actor `cli`. ## Commands | Command | What it does | |---|---| | `sdh sites` | lists the sites | | `sdh create ` | creates a site | | `sdh deploy --site [--draft] [--title "text"]` | publishes; a folder uploads only changed files; with `netlify.toml` / `seedrop.toml` it publishes the `publish` folder and carries the functions | | `sdh drop ` | publishes without an account (if the server allows it); expires in 24 h | | `sdh reivindicar [--nome name]` | claims a drop into the account | | `sdh deploys ` | deploy history | | `sdh publish ` | publishes a draft or rolls back to an earlier deploy | | `sdh env ` | lists variables (secret values masked) | | `sdh env:set KEY value [--public]` | sets a variable; secret unless `--public` | | `sdh domain:add ` | connects a domain and prints the DNS records | | `sdh domain:check ` | checks the DNS and says the next step | | `sdh forms ` | forms and totals | | `sdh leads [--form name]` | submissions received | | `sdh conta` | the account of the token and its plan (JSON) | | `sdh uso` | plan, limits and usage of the month: sites, bandwidth, leads; warnings and next steps | | `sdh plano` | subscription: state, paid until, next charge, open invoice and prices | | `sdh assinar [--anual] --nome "Name" --cpf --email ` | subscribes or changes plan; prints the Pix code and opens the payment page (Pix, boleto or card; `--nao-abrir` only prints the link) | | `sdh cancelar-assinatura --sim` | stops the charge; the plan lasts until the end of the paid period and nothing is deleted | | `sdh cobranca` | (administrator) what is missing to charge: Asaas key, webhook token, webhook | | `sdh cobranca:webhook [--email x]` | (administrator) registers only the SeeDropHost webhook on Asaas | | `sdh tokens` | the tokens of the account, without their secret | | `sdh token:criar [label]` | creates a token; the secret is shown once | | `sdh token:revogar ` | revokes a token at once (the `id` from `sdh tokens`) | | `sdh exportar [file.zip]` | "take everything": site files, `leads.csv`, variable names | | `sdh comparar [--max 500] [--json file.json] [--projeto folder] [--previa]` | migration check, page by page | ## Account, usage and tokens `sdh uso` shows the plan and the month so far. Sites, bandwidth and leads are counted; deploys never are. Going over bandwidth or leads only prints a warning: no site goes offline and every lead is kept. ```bash SDH_TOKEN=sdh_… npx sdh uso # Conta Client Ltda — plano Grátis — mês 2026-10 # sites: 1 de 3 # banda: 0,05 GB de 30 GB # leads: 4 (limite: 100) # deploys: 6 neste mês (deploy nunca conta para limite) # estado: dentro do plano # # Próximos passos: # - nada a fazer: a conta está dentro do plano. Deploys são ilimitados. ``` Give each machine or agent its own token, so one can be revoked without touching the others. A token is created with the token you already have; the account allows up to 20 active tokens. ```bash npx sdh token:criar ci # ✓ token tok-… (ci) criado # sdh_… # guarde agora: ele não aparece de novo. Use como SDH_TOKEN. npx sdh tokens # ids, labels, last use, revoked ones (kept for 90 days) npx sdh token:revogar tok-… # stops working at once ``` Creating a site beyond the plan's site count fails with a message that says what to do (delete a site you no longer use or change plan); nothing that is live is touched. ## Take everything ```bash npx sdh exportar my-site # my-site-2026-10-31.zip in the current folder npx sdh exportar my-site backup.zip ``` The zip has `site/` (the live deploy's files, with `_redirects`, `_headers` and `netlify.toml` / `seedrop.toml` if the site had them), `leads.csv` (opens in Excel and Google Sheets), `variaveis.txt` (variable names only; values never leave) and `LEIA-ME.txt`. An anonymous drop has nothing to export: claim it first with `sdh reivindicar`. ## Deploy The type is decided by what is on disk: a folder, a file ending in `.zip`, or a file ending in `.html` / `.htm`. ```bash npx sdh deploy ./dist --site my-site npx sdh deploy site.zip --site my-site --draft --title "new pricing section" npx sdh deploy index.html --site my-site ``` When uploading a folder, files and folders whose name starts with `.` are skipped (except `.well-known`), and so is `node_modules`. The server applies the same rule to a `.zip`. If the folder has a `seedrop.toml` or `netlify.toml` (`seedrop.toml` wins), only its `[build] publish` folder goes live; the config file and the functions folder go with it but are never served. Point the CLI at the repository root and nothing in the project needs to change. The CLI does not run the build command. The output shows the deploy id, files sent, the config file read and the published folder, the functions found (they answer only if the server has `SDH_FUNCTIONS=1`), the preview and live URLs, the SEO/GEO score, the forms found, and any rule that was not understood. ## Drop and claim ```bash npx sdh drop ./dist # ✓ no ar: http://drop-9d4755b89c.localhost:8787 # código para reivindicar (guarde, não aparece de novo): ab0e… npx sdh reivindicar ab0e… --nome my-site ``` The CLI refuses a drop above 25 MB before sending it. A folder is zipped in memory (no compression) and sent in one request. ## Migration check ```bash npx sdh comparar https://www.current-site.com https://--my-site.your-server.example --projeto ./my-repo --json report.json ``` It discovers pages from the sitemap and from links (up to `--max`, default 500, per site) and compares each address: - **blocks the switch**: status, redirects, canonical, noindex, hreflang, forms, and a new `robots.txt` `Disallow` that covers existing pages; - **attention**: title, description, h1, JSON-LD, text, other `robots.txt` changes and URLs that left the sitemap. Options: - `--projeto `: also checks the old addresses listed in that project's `_redirects` and `netlify.toml`, so a redirect you forgot shows up. - `--previa`: the new site is a draft preview, so the `noindex` SeeDropHost adds to previews does not count. Preview addresses (`--.`) are recognised without the flag. A `noindex` your own site declares still blocks. - `--json `: writes the full list to that file (the file name is required). If the current site's home redirects to another host (bare domain ↔ `www`), the check follows it once and says so. It prints up to 30 problems and exits with code 1 when anything blocks — handy in CI before changing DNS. --- # MCP server for AI agents Checked against the code on 2026-10-31 (version 0.1.0). SeeDropHost has an MCP server so an agent (Claude Code, or any MCP client) can create sites, deploy, read the SEO/GEO report, connect domains, read leads, check the plan usage and the subscription, and export a site. It comes in two forms with almost the same tools. | | Local MCP | Remote MCP | |---|---|---| | Transport | stdio | Streamable HTTP, `POST /mcp`, stateless | | Auth | `SDH_TOKEN` in the environment | `Authorization: Bearer ` | | Which token | an account token (`sdh_…`) or the server token | an account token (`sdh_…`) or the server token | | Reads your disk | only folders listed in `SDH_MCP_ROOTS` | never — it publishes what the agent sends | | Tools | 20 | 18 (no `deploy_directory`, `deploy_zip`) | | Audit actor | `mcp:claude` | `mcp-remoto` | > **In progress:** OAuth 2.1 sign-in for the remote MCP. Today it uses the server's Bearer token. ## Which token to give an agent The MCP uses the same Bearer token as the [HTTP API](/docs/api/). With an **account token** (`sdh_…`) the agent sees and changes only the sites of that account; a site of another account answers "not found", through the MCP too. With the server token it sees every account. Give each agent its own account token (`sdh token:criar claude`, see the [CLI](/docs/cli/)) so it can be revoked on its own. ## The rule: agents deploy drafts Every deploy tool (`deploy_files`, `deploy_directory`, `deploy_zip`) creates a **draft**. Nothing changes for visitors. The tool returns the preview URL, the SEO/GEO score and the main issues, the forms found and the next step. Only `publish_deploy` puts a deploy live — and it is marked destructive, so the MCP client asks a person first. `set_env_var` and `delete_env_var` are destructive too, because a replaced or deleted secret cannot be brought back. ## Connect Claude Code ### Local (stdio) From the repository folder, with the server running: ```bash claude mcp add seedrophost \ -e SDH_TOKEN=dev-token -e SDH_API_URL=http://localhost:8787 -e SDH_MCP_ROOTS=$PWD \ -- node --disable-warning=ExperimentalWarning apps/mcp/src/main.ts ``` `SDH_MCP_ROOTS` is a `:`-separated list of folders the agent may publish from (default: the current folder). ### Remote (Streamable HTTP) ```bash claude mcp add --transport http seedrophost https://your-server.example/mcp \ --header "Authorization: Bearer YOUR-TOKEN" ``` Without a valid token the endpoint answers `401` with `WWW-Authenticate: Bearer realm="seedrophost"`. Only `POST` is accepted (no sessions). The remote `compare_sites` refuses addresses outside the public internet, checked again at connection time. Then ask, for example: "list my sites", "publish this HTML as a draft on site X", "show this week's leads". ## Tools | Tool | What it does | Annotation | |---|---|---| | `list_sites` | sites with address and live deploy | read-only | | `get_site` | address, live deploy and domains of one site | read-only | | `create_site` | creates an empty site | write | | `deploy_files` | draft deploy from file contents (1–500 files, utf8 or base64); only changed files upload | write | | `deploy_directory` | draft deploy from a local folder inside `SDH_MCP_ROOTS` (local only) | write | | `deploy_zip` | draft deploy from a local `.zip` inside `SDH_MCP_ROOTS` (local only) | write | | `list_deploys` | latest deploys, which one is live, score of each | read-only | | `get_deploy_report` | SEO/GEO report, forms, redirect rules, changed pages | read-only | | `publish_deploy` | puts a ready deploy live: approves a draft or rolls back | **Destructive** | | `list_env_vars` | variables, secret values masked | read-only | | `set_env_var` | creates or replaces a variable (encrypted); a replaced secret cannot be recovered | **Destructive** | | `delete_env_var` | removes a variable | **Destructive** | | `add_domain` | connects a domain and returns the DNS records | write | | `check_domain` | looks up DNS: verified, routed, next step | write, open world | | `list_forms` | forms with fields, totals, spam blocked | read-only | | `compare_sites` | migration check between the current site and the new one; `previa` for a draft preview (detected automatically for `--.` addresses); `projeto` (local only) also checks the old addresses in a project's `_redirects` / `netlify.toml` | read-only, open world | | `list_leads` | form submissions, latest first | read-only | | `get_usage` | plan of the account, limits, usage of the month (sites, bandwidth, leads), state and next steps; deploys never count, and going over only warns | read-only | | `get_billing` | subscription of the account (`gratis`, `pendente`, `ativo`, `atrasado` or `cancelado`), plan, paid until, next charge, open invoice link and prices. Subscribing needs a CPF and cancelling stops the charge, so both stay with the person: the agent points to the dashboard ("Mudar de plano") or `sdh assinar` / `sdh cancelar-assinatura` | read-only | | `export_site` | summary of the "take everything" export of a site (files, leads, variable names; values never leave) and how to download the zip (`GET /v1/sites/{site}/export` or `sdh exportar`); it does not download the zip itself | read-only | "Destructive" means the tool is registered with `destructiveHint: true`; "read-only" means `readOnlyHint: true`. Every call goes through the HTTP API and is written to the audit log. ## Typical agent flow 1. `create_site` (once; beyond the plan's site count it answers that the plan is full, and nothing live is touched). 2. `deploy_files` → draft + preview URL + score. 3. Fix what `get_deploy_report` lists, deploy again (only changed files upload). 4. For a migration, `compare_sites` between the current site and the preview URL. 5. Show the preview to the person; on their approval, `publish_deploy`. 6. Rolling back is `publish_deploy` with an earlier `deploy_id` from `list_deploys`. 7. `get_usage` any time to see how the month is going; `export_site` before a migration away, to hand the person their files and leads. --- # Redirects and headers Checked against the code on 2026-10-30 (version 0.1.0). Rules can come from two places, with the same behaviour: 1. a `_redirects` file and a `_headers` file at the root of the deploy; 2. `[[redirects]]` and `[[headers]]` in `netlify.toml` or `seedrop.toml` (same format; `seedrop.toml` wins if both exist). These come **after** the `_redirects` / `_headers` rules. Lines that are not understood are listed in the deploy report under `rules.problems` (file and line) and are ignored. The rule files themselves (`_redirects`, `_headers`, `netlify.toml`, `seedrop.toml`) are never served as public files. ## _redirects One rule per line: `from to [status][!] [Condition=a,b]`. Text after `#` is a comment. ```text # permanent redirect (301 is the default) /old-page /new-page /blog/* /articles/:splat 301 /products/:id /p/:id 302 # rewrite: serve another file, URL stays the same /app/* /app/index.html 200 # custom 404 for a section /docs/* /docs/404.html 404 # force: applies even if /promo.html exists /promo /sale 302! # only for requests that arrive on this host https://example.com/* https://www.example.com/:splat 301! # conditions / /pt/ 302 Language=pt / /br/ 302 Country=br # proxy to a public address /api/* https://api.example.com/:splat 200 # send to a function (needs SDH_FUNCTIONS=1 on the server) /api/* /.netlify/functions/:splat 200 ``` ### What is supported - **Status codes:** 200, 301, 302, 303, 307, 308, 404, 410. Default: 301. - **Splat:** `*` captures the rest of the path; use it as `:splat` in the target. `/blog/*` also matches `/blog`. - **Placeholders:** `:name` captures one path segment. - **Absolute origin:** `https://www.example.com/*` only matches requests whose `Host` is that name. The protocol only counts when the server knows it (TLS on the socket, or `X-Forwarded-Proto` with `SDH_TRUST_PROXY=1`). - **Order:** the first matching rule wins; `_redirects` before the config file. - **Shadowing:** a rule without `!` does not apply when the requested path exists as a file (404 rules always apply). Add `!` to force it. - **Query string:** kept on redirects when the target has none. - **Conditions:** `Language=` matches the `Accept-Language` header (`pt` also matches `pt-BR`). `Country=` uses the `CF-IPCountry` header, so it only means something with Cloudflare in front — do not use it for anything that must be secure. - **Targets:** a path starting with `/` (not `//`), or a full `http://` / `https://` address. Anything else (`//host`, `javascript:`…) is refused with a problem in the report. If a path target turns into an outside address after `:splat` is filled in, the rule is ignored. - **Proxy:** a 200 rule whose target is `http(s)://…` fetches that address and returns its answer. Only public internet addresses are allowed — checked again at connection time, so DNS cannot switch to a private address afterwards — with no redirect following, a 10-second timeout and a 10 MB limit read as a stream. Only `GET`/`HEAD` reach it, and only the `Accept` header is forwarded. ### Not supported - Other conditions (`Role=`, `Cookie=`) and query-parameter matching such as `id=:id`: the rule is parsed but never matches. Nothing redirects by mistake. - In `netlify.toml`: `query`, `signed` and `headers` inside `[[redirects]]` are ignored with a warning in the report. - Anonymous drops ignore rules that point outside the site. ## _headers A path line (starting with `/`, wildcards and `:placeholders` allowed) followed by indented `Name: value` lines. ```text /* X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin /assets/* Cache-Control: public, max-age=31536000, immutable /*.md Content-Type: text/markdown; charset=utf-8 ``` The same in `netlify.toml`: ```toml [[headers]] for = "/assets/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" ``` All matching blocks apply, in order; a later value replaces an earlier one with the same name. - **`Set-Cookie` is always dropped** (with a problem in the report): every site shares the base domain with the dashboard, so a cookie could leak to other sites. Use a function to set cookies. - On anonymous drops, `Content-Security-Policy`, `Service-Worker-Allowed` and `Access-Control-*` are dropped too, and the page is always `noindex`. ### Headers SeeDropHost sets by itself | Header | Value | |---|---| | `Content-Type` | from the file extension (HTML, CSS, JS, JSON, TXT, XML, SVG, images, fonts, PDF, MP4, webmanifest); others `application/octet-stream` | | `ETag` | from the content hash; `If-None-Match` answers `304` | | `Cache-Control` | `public, max-age=0, must-revalidate` | | `X-Content-Type-Options` | `nosniff` | | `X-SDH-Deploy` | id of the deploy that answered | | `X-Robots-Tag` | `noindex` on drafts, older deploys and anonymous drops | | `X-SDH-Preview` | `1` when that `noindex` was added by SeeDropHost (not by your `_headers`) — the migration check uses it | ## Clean URLs, trailing slash and 404 - `/about` serves `/about.html`; `/docs/` serves `/docs/index.html`. - Each page has one address: if only `about/index.html` exists, `/about` answers `301` to `/about/`; if only `about.html` exists, `/about/` answers `301` to `/about`. The query string is kept. - When nothing matches, `/404.html` is served with status 404 if it exists. --- # Forms and leads Checked against the code on 2026-10-30 (version 0.1.0). A static site can receive form submissions without a backend. You mark the form in the HTML; SeeDropHost finds it when you deploy, receives the submissions and keeps them in a lead inbox. ## Mark the form Either the native attribute: ```html
``` or the attribute many sites already use (handy when migrating): ```html
``` The form name is `data-sdh-form`, or `name` when `data-netlify="true"` (or a bare `netlify` attribute) is used. The deploy report lists the forms found (`report.forms`). Forms are detected in HTML files up to 2 MB. ## What happens when someone submits 1. The browser sends `POST` to any page of the site. When serving the page, SeeDropHost adds a hidden `form-name` field to each detected form, so you do not have to. 2. Bodies are accepted as `application/x-www-form-urlencoded` or JSON, up to 1 MB. Each value is cut at 10 000 characters. File uploads (`multipart/form-data`) are not supported. 3. The answer is a `303` redirect to the form's `action` when it is a path on the same site (for example `/thanks`; `//host` and `/\host` do not count), otherwise to the same page with `?enviado=1`. With `Accept: application/json` the answer is `{"ok":true}`. ## Antispam - **Honeypot:** if the form declares one (`data-sdh-honeypot="field"` or `netlify-honeypot="field"`), that field is the trap. If it does not, SeeDropHost injects its own hidden field when serving the page. A filled honeypot marks the submission as spam. - **Links:** more than 3 `http(s)://` links in one submission marks it as spam. - **Rate limit:** 10 submissions per minute per IP per site; above that, `429` with `Retry-After: 60`. - Spam gets the same success answer, so senders do not learn how to get around it. It is kept apart and hidden by default. - IP addresses are not stored: only a keyed hash (HMAC-SHA256, per site, with a key derived from the server's master key), so the address cannot be recovered by trying every IP. - Anonymous drops do not accept submissions (`403`) until they are claimed. ## Read the leads ```bash # API curl -H "Authorization: Bearer $SDH_TOKEN" "$SDH_API_URL/v1/sites/my-site/submissions?form=contact" # CLI npx sdh leads my-site --form contact ``` From an agent: the MCP tools `list_forms` and `list_leads`. The dashboard shows them too. Add `spam=1` to the API call to include spam. ## Not available yet - Email or webhook notification of new leads. - CSV export (the API returns JSON). - CAPTCHA. --- # Migrate from Netlify Checked against the code on 2026-10-30 (version 0.1.0). SeeDropHost does not run your build command. If your site needs a build, build it as you do today; then point `sdh deploy` at the repository root (it reads `netlify.toml` and publishes the `publish` folder) or straight at the built folder. ## What is read automatically | In your project | What SeeDropHost does | |---|---| | `netlify.toml` or `seedrop.toml` | `[build] publish`: only that folder goes live. `[[redirects]]` (from, to, status, force, Language/Country conditions, including `from = "https://www.example.com/*"`) and `[[headers]]` (`for` + `[headers.values]`). `seedrop.toml` wins if both exist. The file itself is never served. | | `_redirects` | read as is: 200/301/302/303/307/308/404/410, `*` and `:splat`, `:placeholders`, `!` to force, `Language=` and `Country=`, proxy to public addresses. [Details](/docs/redirects-headers/) | | `_headers` | read as is: path blocks with wildcards, `Name: value` lines (`Set-Cookie` is dropped) | | `` | detected on deploy; submissions go to the lead inbox. `netlify-honeypot="field"` is respected; `form-name` is added for you. [Details](/docs/forms/) | | Functions in `[functions] directory` (default `netlify/functions`) | carried with the deploy and run only if the server has `SDH_FUNCTIONS=1` — see below | | `404.html` | served with status 404 for unknown paths | | Clean URLs | `/about` serves `about.html` or `about/index.html`; the other form answers 301 to the one that exists | The same layout is applied to a `.zip` of the repository root and to a folder dropped in the dashboard. ## Functions: opt-in, for a single owner Supported formats, one file per function (`.js`, `.mjs` or `.cjs`): ```js // classic exports.handler = async (event, context) => ({ statusCode: 200, body: "ok" }); // modern export default async (req) => new Response("ok"); ``` They answer at `/.netlify/functions/` and `/.sdh/functions/`, and through 200 rules such as `/api/* /.netlify/functions/:splat 200`. Limits: 10 seconds, 128 MB of memory, 6 MB request and 6 MB response. `process.env` holds only the site's variables with the `functions` scope. > **Warning:** functions are **off by default**. Turn them on (`SDH_FUNCTIONS=1`) only if you own every site on the server. A function runs in a worker of the same process with time and memory limits, but that is **not isolation**: it can read the database, the files of every site and the server credentials. Whoever publishes a function has full access to the server. With functions off, those paths answer 404. ## What is not read yet - No build from Git and no deploy on push. `[build] command`, plugins and `[context.*]` are not run. Build locally or in CI, then run `sdh deploy`. - Edge functions, scheduled functions and background functions. - Functions that `require` neighbouring files or `node_modules` (each function is one file), and functions in TypeScript (reported as a warning). - In `[[redirects]]`: `query`, `signed`, `headers` and the `Role` condition are reported as warnings and ignored. `_redirects` conditions other than `Language` and `Country` never match. - Forms: no email notifications, no file uploads, no reCAPTCHA. Spam is filtered by honeypot, link count and rate limit. - No global CDN of its own: put Cloudflare in front ([self-host guide](/docs/self-host/)). Every one of these shows up in the deploy report, so nothing is skipped silently. ## Step by step 1. Build the site as you do today, for example `npm run build`. 2. Create the site and upload it as a **draft**. A draft gets its own preview address and does not go live; your domain still points to Netlify, so visitors see no change. ```bash npx sdh create my-site npx sdh deploy ~/my-repo --site my-site --draft ``` 3. Read the report the command prints: the config file it read, the published folder, functions found, SEO/GEO score, forms found and any rule that was not understood. 4. Compare the live Netlify site with the draft preview, page by page, including the old addresses listed in your `_redirects` and `netlify.toml`: ```bash npx sdh comparar https://www.your-site.com https://--my-site.your-server.example --projeto ~/my-repo --json report.json ``` It blocks (exit code 1) when status, redirects, canonical, noindex, hreflang, forms or a new `robots.txt` Disallow over existing pages differ, and warns about title, description, h1, JSON-LD, text and URLs missing from the sitemap. A preview address (`--.`) is recognised automatically: the `noindex` every preview carries does not count, but a `noindex` your site declares itself still blocks. Use `--previa` to force this for another address. If the old site's home redirects to another host (bare domain ↔ `www`), the comparison follows it once and says so. 5. Fix, deploy again, compare again until nothing blocks. Then publish: `npx sdh publish my-site `. 6. Connect the domain: `npx sdh domain:add my-site www.your-site.com`. Create the TXT record it prints (proof that the domain is yours) and the CNAME (or A records for the bare domain). Check with `npx sdh domain:check www.your-site.com`. The domain is served only after the TXT is verified. 7. Keep the Netlify site until DNS has switched and the leads arrive in the new inbox (`npx sdh leads my-site`). ## Undo Point DNS back to Netlify. On SeeDropHost every deploy stays available; `sdh publish` with an earlier id rolls back instantly. --- # Self-host on a VPS Checked against the code on 2026-10-30 (version 0.1.0). Today SeeDropHost runs on your own server: there is no public hosted instance yet. This page summarises the full step-by-step guide (in Portuguese) that ships with the code as `docs/VPS.md`. It takes about 30 minutes and is copy-and-paste. ## What you need - A VPS with **Ubuntu 24.04** and **2 GB of memory**. A data centre close to your visitors helps. - A domain, for example `mysites.example`. - Optional but recommended: a free Cloudflare account in front. ## 1. DNS (with Cloudflare in front) | Type | Name | Content | Proxy | |---|---|---|---| | A | `@` | your server IP | proxied | | A | `*` (every site) | your server IP | proxied | | A | `edge` | your server IP | **DNS only** | `edge` is where your customers' domains point (CNAME); it stays "DNS only" so domains in other Cloudflare accounts can point to it. Leave **Always Use HTTPS off** — the server redirects to HTTPS itself, and the Let's Encrypt HTTP challenge for customer domains must reach port 80. **Wildcard origin certificate.** In Cloudflare, create an origin certificate for `mysites.example` and `*.mysites.example`. After step 2, paste it into `/etc/seedrophost/tls/origin.pem` and `origin.key` and run the installer again. Use SSL/TLS mode **Full** until it is installed, then **Full (strict)**. ## 2. Install ```bash ssh root@YOUR-SERVER-IP apt-get update && apt-get install -y git git clone /opt/seedrophost sudo bash /opt/seedrophost/deploy/install.sh mysites.example you@example.com ``` The installer: - installs Node.js 24 (official package, SHA256 checked) and Caddy; - creates the `seedrophost` user, the data folder `/var/lib/seedrophost`, the settings file `/etc/seedrophost/env`, and a random **token** and **master key** in `/etc/seedrophost/credentials/` (readable by root only, never in the repository), handed to the service as systemd credentials; - starts a hardened systemd service (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome` and more), the daily backup and a firewall (ports 22, 80, 443). Running it again is safe: existing secrets are kept. Read your token with `sudo cat /etc/seedrophost/credentials/sdh_token`. > **Warning:** store the master key (`/etc/seedrophost/credentials/sdh_master_key`) in a password manager. Without it, secret site variables cannot be read back, not even from a restored backup. ## 3. HTTPS Caddy sits in front. The base domain, every `.`, every deploy preview and the drop domain share the **wildcard certificate** from step 1, so no certificate is requested per site. Customer domains get a Let's Encrypt certificate on their first visit (on-demand TLS), but only when Caddy asks SeeDropHost and the domain is **already verified by TXT**; the dashboard host gets one the same way when it is outside the base. Anything else gets no certificate, so nobody can exhaust your certificate quota by pointing random names at the server. SeeDropHost itself listens only on `127.0.0.1`. ## Dashboard, sites and anonymous drops - The installer sets `SDH_PANEL_HOST` to the base domain: the dashboard, API and MCP answer only there, and no site is ever served on that host. Sites live at `.`. - Publishing without an account (`SDH_ANON_DROP=1`) is off by default. If you turn it on, give it a separate domain with `SDH_DROP_DOMAIN` (same `@` and `*` records, and its own origin certificate in `drop-origin.pem` / `drop-origin.key`), so a phishing page published anonymously never carries your main domain. [Drop limits](/docs/limits/). - Optional: listing the drop domain (or the base) in the Public Suffix List makes browsers treat each site as a separate site. It takes weeks. ## 4. First deploy Open `https://mysites.example`, sign in to the dashboard with the token and drop a folder, or from your computer: ```bash export SDH_API_URL=https://mysites.example export SDH_TOKEN=paste-the-token npx sdh create my-site npx sdh deploy ./site-folder --site my-site ``` The site is at `https://my-site.mysites.example`. ## 5. Remote MCP ```bash claude mcp add --transport http seedrophost https://mysites.example/mcp --header "Authorization: Bearer YOUR-TOKEN" ``` Everything Claude does is in the audit log as `mcp-remoto`. The remote MCP does not read files on the server. [MCP details](/docs/mcp/). ## 6. Backup, restore, update, take down - **Backup:** every day at 03:30 the server copies the database and site files to `/var/backups/seedrophost/seedrop-YYYYMMDD-HHMMSS` and deletes copies older than 14 days. Run one now with `sudo bash /opt/seedrophost/deploy/backup.sh`. Keep a copy off the VPS (provider snapshots or `scp`). - **Restore:** stop the service, copy the database and the `blobs/` folder back, fix ownership, start the service (exact commands in `docs/VPS.md`). On a new server, put the same master key and token in `/etc/seedrophost/credentials/`. - **Update:** `sudo bash /opt/seedrophost/deploy/update.sh` backs up, fetches the new version, installs, runs the tests as the `seedrophost` user (never root) and restarts only if they pass. - **Take a site down at once** (phishing, abuse): `DELETE /v1/sites/{site}` with the token. It removes the site with its deploys, domains, forms and leads, and is recorded in the audit log. ## 7. Configuration | Variable | What it is | |---|---| | `SDH_TOKEN` | API token (required outside dev mode; from systemd credentials on the VPS) | | `SDH_MASTER_KEY` | encryption key for site variables (required outside dev mode; from systemd credentials on the VPS) | | `SDH_BASE_DOMAIN` | sites live at `.` | | `SDH_PANEL_HOST` | the only host for dashboard, API and MCP; never serves a site | | `SDH_PORT` / `SDH_HOST` | port and listen address (`127.0.0.1` behind Caddy) | | `SDH_EDGE_CNAME` / `SDH_EDGE_IPV4` | what customers point their DNS to | | `SDH_DATA_DIR` | database and files | | `SDH_ANON_DROP=1` | allows publishing without a token (off by default) | | `SDH_DROP_DOMAIN` | separate domain for anonymous drops | | `SDH_ANON_DROP_MAX_HOUR` / `SDH_ANON_DROP_MAX_MB` | server-wide drops per hour (default 60) and total drop storage in MiB (default 2048) | | `SDH_FUNCTIONS=1` | runs functions — only if you own every site ([why](/security/#functions)) | | `SDH_TRUST_PROXY=1` | read the visitor IP from `X-Forwarded-For` (only behind your own proxy) | | `SDH_ACCESS_LOG=1` | one JSON line per request, with a per-process IP alias instead of the raw IP | Health check: `curl https://mysites.example/v1/health`. ## Known risks | Risk | What to do | |---|---| | A phishing page on an anonymous drop stays up to 24 hours | delete it with `DELETE /v1/sites/{site}`; use `SDH_DROP_DOMAIN` | | `x-sdh-actor` in the audit log is declared by the client | treat it as a label; the token is the identity | | `Country=` rules trust `CF-IPCountry` | without Cloudflare in front, visitors can fake it | | Rate counters live in memory | a restart resets them | | Functions are not isolated | turn them on only if you own every site | ## Costs Your server and domain. The guide estimates about R$ 35–80 per month (October 2026 prices, Brazil) for a 2 GB VPS, provider backup and a `.com.br` domain, with Cloudflare's free plan and free Let's Encrypt certificates. Hosted SeeDropHost plans: see [pricing](/pricing/). --- # Limits Checked against the code on 2026-10-31 (version 0.1.0). Every number below is a constant in the source; MB here means the unit written in the code (decimal "100 MB" = 100 000 000 bytes; "MiB" = 1 048 576 bytes). > **Note:** plan limits are in the code as data and are measured (see "Plans and accounts" below), and billing by Pix and boleto is in the code too, off until the administrator sets the Asaas key ([billing API](/docs/api/#billing)); the hosted plans open with the first public server ([pricing](/pricing/)). On your own server, disk and network are the real limit. ## Deploys (with a token) | Limit | Value | |---|---| | Request body (zip, HTML, one uploaded file, JSON) | 100 MB (100 000 000 bytes) | | Files per deploy | 50 000 | | Path length | 1 024 characters; no `.`/`..` or empty segments, no `\`, no NUL | | HTML analysed for the SEO/GEO report and forms | files up to 2 MB (2 000 000 bytes); larger files are served but not analysed | | `title` / `source` query options | 200 / 32 characters | | `x-sdh-actor` header | 64 characters | | Deploys kept | all; 0.1.0 never deletes deploys automatically | ## .zip uploads | Limit | With a token | Anonymous drop | |---|---|---| | Entries in the zip | 20 000 | 20 000 | | Total size uncompressed | 500 MiB | 50 MiB | | One file uncompressed | 100 MiB | 25 MiB | | Compression ratio (files above 1 MiB) | at most 200:1 | at most 200:1 | Also: only "stored" and "deflate" compression; no ZIP64; no multi-volume zips; no password-protected entries; the CRC32 of each file is checked. `__MACOSX/` and common junk files are skipped. When every file sits in one top-level folder, that folder becomes the site root. ## Publishing without an account (drop) | Limit | Value | |---|---| | Availability | off unless the server runs with `SDH_ANON_DROP=1` | | Request size | 25 MiB (the CLI checks before sending) | | Rate | 5 per network per hour (one IPv4 address, or one IPv6 `/64`); every attempt counts, including refused ones (`429`, `Retry-After: 3600`) | | Server-wide cap | `SDH_ANON_DROP_MAX_HOUR` drops per hour for the whole server, default 60 (`429`, `Retry-After: 600`) | | At the same time | 2 drops processed at once; more answer `503` (`busy`, `Retry-After: 10`) | | Total storage | `SDH_ANON_DROP_MAX_MB`, default 2048 MiB for all drops together (`503`, `drop_quota`, `Retry-After: 3600`) | | Lifetime | 24 hours unless claimed; expired drops answer `410` and are removed within about 10 minutes | | Restrictions until claimed | always `noindex`, forms off (`403`), redirects/proxy to other sites ignored, no functions, no CSP / `Service-Worker-Allowed` / `Access-Control-*` headers | | Claim code | shown once; only its SHA-256 is stored | Rate counters (drops and forms) live in the server's memory and start again from zero when it restarts. ## Forms | Limit | Value | |---|---| | Submission body | 1 MB (1 000 000 bytes) | | One field value | 10 000 characters (longer values are cut) | | Rate | 10 submissions per minute per IP per site (`429`, `Retry-After: 60`) | | Links | more than 3 `http(s)://` links → marked as spam | | Detection | up to 200 forms per page, 200 fields per form | ## Proxy rules in _redirects | Limit | Value | |---|---| | Target | public IP addresses only | | Timeout | 10 seconds | | Target address | checked again at connection time; redirects are not followed | | Answer size | 10 MB (10 000 000 bytes), counted while reading | ## Functions (only with `SDH_FUNCTIONS=1`) | Limit | Value | |---|---| | Time | 10 seconds (`SDH_FUNCTIONS_TIMEOUT_MS` can lower it, never raise it); then `504` | | Memory | 128 MB per worker | | Request body | 6 MiB (`413` above it) | | Response body | 6 MiB | | Running at once | 32 workers for the whole server; more answer `503` | | Function name | `A-Z`, `a-z`, `0-9`, `_`, `-`, 1–64 characters | | Code | one `.js`, `.mjs` or `.cjs` file per function, no `require` of other files or `node_modules` | ## Plans and accounts The numbers of the three plans live in `packages/core/src/plans.ts`. Sites that existed before accounts, and sites created with the server token, belong to the `principal` account, which is on the Agency plan. | Item | Free | Pro | Agency | |---|---|---|---| | Sites in the account (anonymous drops do not count) | 3 | 25 | 150 | | Bandwidth per month (soft limit: a warning, never a block) | 30 GB | 300 GB | 2 048 GB | | Form leads per month (a lead over the limit is always kept) | 100 | unlimited | unlimited | | Deploys | unlimited | unlimited | unlimited | - 1 GB here is 1 024³ bytes. Months are counted in UTC. - **Sites** are the only hard limit: creating or claiming a site beyond the plan answers `402 plan_limit`, and nothing that is live is touched. - **Bandwidth and leads** only produce a warning (`estado: aviso`). Over a limit for two months in a row, a plan change is requested with 14 days to do it (`estado: pedir_troca`). No site is switched off and no lead is lost. - **Failed payment:** when an invoice is overdue the state is recorded and there are 30 days to pay with everything as it is. After that the sites still stay live and nothing is deleted; only creating a new site follows the Free limit (3 sites) until the invoice is paid. - **Subscription attempts:** 5 new checkouts per account in 24 h (`429 checkout_limit`); repeating the same request with the same `Idempotency-Key` does not count. - Bandwidth is summed in memory and written once a minute, without IP or path. - Read it all with `GET /v1/account/usage`, `sdh uso` or MCP `get_usage`. | Item | Rule | |---|---| | Active tokens per account | 20 (the 21st answers `409 token_limit`) | | Revoked tokens | kept in the list for 90 days, then removed | | Token label / account name | 100 characters | ## Names and keys | Item | Rule | |---|---| | Site name | 1–63 characters, `a-z`, `0-9`, `-`; not starting or ending with `-`; no `--` | | Reserved site names | `www`, `api`, `app`, `admin`, `edge`, `mcp`, `status`, `docs`, `mail`; names starting with `site-`, `dpl-`, `acc-` or `tok-` (they look like internal ids) | | Variable name | `A-Z`, `0-9`, `_`, not starting with a digit, up to 128 characters | ## Lists | List | Range (default) | |---|---| | `GET /v1/sites/{site}/deploys?limit=` | 1–100 (20) | | `GET /v1/sites/{site}/submissions?limit=` | 1–500 (50) | | `GET /v1/audit?limit=` | 1–500 (50) | | MCP `deploy_files` | 1–500 files per call | | MCP `list_deploys` / `list_leads` | 1–100 (10) / 1–200 (50) | | `sdh comparar --max` / MCP `compare_sites` | pages per site, default 500; MCP accepts 1–2 000 | ## Not limited in code Number of `_redirects` / `_headers` rules, number of custom domains per site and storage. These are bounded only by your server. Sites and bandwidth are bounded by the plan of the account, as described above. --- # Security Checked against the code on 2026-10-31 (version 0.1.0). This page describes what the code does today. It is not a certification; there has been no external audit yet. ## Report a problem Email **security@seedrophost.com** with steps to reproduce. Please do not open a public issue for a vulnerability. Machine-readable contact: [/.well-known/security.txt](/.well-known/security.txt). Abuse of a published site (phishing, malware) goes to the same address; the operator can take a site down at once with `DELETE /v1/sites/{site}`. ## Secrets - Site variables are encrypted at rest with **AES-256-GCM** under the server's master key (`SDH_MASTER_KEY`). - Secret values are **always masked** in API, CLI, MCP and dashboard answers (for example `s3c••••••ue`) and are not written to logs. - Outside development mode the server refuses to start without its own `SDH_TOKEN` and `SDH_MASTER_KEY`. The self-host installer generates both at random and hands them to the service as systemd credentials (`LoadCredential=`), outside the process environment. - The server token (`SDH_TOKEN`) is compared in constant time. Account tokens are described under "Accounts and isolation". ## Who did what Every write (site, deploy, publish, variable, domain, drop, claim, site deletion) is recorded in an audit log with the actor — `cli`, `mcp:claude`, `mcp-remoto`, `dashboard`, or whatever the client sends in `x-sdh-actor` — the action, the target and the time. Read it with `GET /v1/audit`. Each entry also records the account. The actor name is declared by the client, not proven; with labelled tokens it tells machines and agents apart, not people. An account token reads only the entries of its own account; the administrator reads all of them. ## Accounts and isolation - **Tokens.** Each account has its own tokens, with the prefix `sdh_` (easy for secret scanners to recognise). The secret is shown **once**, when the token is created; the server stores only its SHA-256. An account has at most 20 active tokens, and revoking one takes effect on the next request. Revoked tokens stay in the list for 90 days and then disappear. The server token (`SDH_TOKEN`) is the administrator: the only one that creates accounts and changes plans. - **One filter for every route.** A route that takes a site or a deploy goes through the account filter before its handler runs, so a new route is protected from the start. A site, deploy, domain request or token of another account answers `404`, the same as one that does not exist, because a `403` would confirm that it exists. An automated test walks the whole route table to check that none escapes the filter. - **Files.** Files are stored once on disk by their SHA-256, but each account has to send the bytes at least once: knowing the hash of another account's file is not enough to use it. - **Names.** A site name that starts with `site-`, `dpl-`, `acc-` or `tok-` is refused, because it looks like an internal id. - **Usage metering.** Bandwidth is summed per site and month in memory and written once a minute, with no IP address and no path. Going over a plan limit only produces a warning: no site goes offline and no lead is lost. The one hard refusal is creating a site beyond the plan (`402`), which touches nothing that is already live. - **Not people.** An account is a group of tokens with labels, not of users: there are no identities or roles inside an account yet. ## Previews are public by link Every deploy, drafts included, gets an address `--.`. It asks for no login: anyone with the link sees the content, as with the deploy previews of other services. The deploy id is random, so it cannot be guessed, and the preview is served with `X-Robots-Tag: noindex`, so search engines do not list it. A link that someone forwards still opens. Do not publish confidential material (a proposal, something before its launch), not even as a draft. ## AI agents - Deploys made through MCP always start as **drafts**; drafts are served with `noindex`. - Tools that change what is live or replace or delete a secret (`publish_deploy`, `set_env_var`, `delete_env_var`) are marked `destructiveHint: true`, so MCP clients ask a person first. - The local MCP reads only folders listed in `SDH_MCP_ROOTS` (real paths are resolved, so symlinks cannot escape). The remote MCP cannot read the server's disk at all. ## Dashboard and sites kept apart - With `SDH_PANEL_HOST` set, the dashboard, API and remote MCP answer only on that host (or to calls from the machine itself), and no site is ever served on it. - With `SDH_DROP_DOMAIN` set, anonymous drops live on a separate domain from your sites. - Dashboard answers carry `frame-ancestors 'none'`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer` and `nosniff`, so the dashboard cannot be framed by another page. - `Set-Cookie` is dropped from every site's `_headers` and `[[headers]]`, because all sites share the base domain. - `_redirects`, `_headers`, `netlify.toml`, `seedrop.toml` and function code are never served as files. ## Server-side request forgery (SSRF) One shared check refuses addresses outside the public internet: private, loopback, link-local (including cloud metadata at `169.254.169.254`), carrier-grade NAT, multicast and reserved IPv4 ranges; IPv6 loopback, unique-local, link-local and multicast; and IPv4 hidden inside IPv6 (`::ffff:…`, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`). - **Proxy rules** check the literal host first, then check again at connection time, so DNS cannot switch to a private address afterwards. No redirects are followed; 10-second timeout; 10 MB answer limit read as a stream. - **The migration check** run through the remote MCP uses the same check. - The internal route Caddy uses before issuing certificates answers only calls from the machine itself without proxy headers; the shipped Caddyfile hides it from the internet. ## Domains and HTTPS - A custom domain is served **only after its TXT record is verified**, and a domain verified by one site cannot be taken by another. - In the self-host setup, the base domain, every site, every preview and the drop domain use **one wildcard certificate** installed by the operator. Caddy issues on-demand certificates only for the dashboard host and verified custom domains; random names pointed at the server get nothing, which protects the Let's Encrypt quota. ## Abuse of anonymous publishing Publishing without an account is **off by default**. When the operator turns it on: - 25 MiB per request; zip limited to 50 MiB uncompressed and 25 MiB per file. - 5 per network per hour (an IPv4 address, or a whole IPv6 `/64`), plus a server-wide cap per hour (`SDH_ANON_DROP_MAX_HOUR`, default 60). - At most 2 processed at the same time, and a total storage quota (`SDH_ANON_DROP_MAX_MB`, default 2048). - Sites expire after 24 hours unless claimed, are always `noindex`, cannot receive form submissions, cannot redirect or proxy to other sites, never run functions, and cannot set CSP, `Service-Worker-Allowed` or CORS headers. - The claim code is shown once and stored only as a hash. A phishing page can stay up until it expires or is deleted; see "Not in place yet". ## Functions Off by default (`SDH_FUNCTIONS=1` turns them on). Even when on, they run only for sites of the main (`principal`) account: sites of customer accounts answer `404` for function routes until functions are isolated. A function runs in a worker of the same process with a 10-second and 128 MB limit and sees only its site's variables, but this is **not isolation**: a function can read the database, every site's files and the server credentials. Turn functions on only if you own every site on the server. ## Uploads Zip files are read with strict limits: entry count, total and per-file size, compression ratio (zip bombs), CRC32 check, no absolute paths, no `..`, no backslashes, no password-protected entries. Deploy paths are validated the same way. ## Forms and privacy - Visitor IP addresses are not stored with submissions: only a keyed hash (HMAC-SHA256 with a key derived from the master key), so the IP cannot be recovered by trying every address. - Form `action` values that point to another host (`//…`) are refused. - The optional access log (`SDH_ACCESS_LOG=1`) prints a per-process alias instead of the IP, and no query string. - Spam receives the same answer as a real submission. ## Hardening of the self-host install systemd service with `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome` and kernel protections; the app listens on `127.0.0.1` behind Caddy; firewall open only on 22, 80 and 443; updates run the tests as the service user, not root; daily backups kept 14 days. ## Not in place yet - External security audit. - People and roles inside an account (today: several labelled tokens, not users). - Isolated functions per account (today only the main account runs functions, and only on a server with a single owner). - OAuth for the remote MCP: **In progress** (a Bearer token today). - Automated abuse scanning of published content. - Rate limits are kept in memory, so they reset when the server restarts. --- # SeeDropHost compared with Netlify, Vercel, Cloudflare and others Checked against the code on 2026-10-31 (version 0.1.0). **Data checked on 2026-10-30.** Competitor cells come from each provider's official pages, read on that date and summarised in our own words; every cell links to its source. Prices and limits change often — open the source before deciding. SeeDropHost cells link to our docs, which are checked against our code. Legend: **Not found** = we looked in the official docs and did not find it (it may still exist). **—** = not covered by our research. ## Where SeeDropHost loses today - **No global CDN of its own.** Your sites are served from your server; put Cloudflare in front. - **No build from Git**, no deploy on push. You upload files that are already built. - **Functions are basic and not isolated.** Single-file `exports.handler` or `export default` functions run only when the operator turns them on, and only for a server with a single owner: a function can read the whole server. Even then only the main account's sites run them; customer accounts get a 404 until functions are isolated. No edge, scheduled or background functions. - **No hosted plan you can sign up for yet**: plans and prices are on the [pricing page](/pricing/), but the hosted service opens only with the first public server. Accounts, plan limits and billing by Pix and boleto already exist in the code. - **No public status page or uptime history** yet. - **Young project**: version 0.1.0, accounts have several labelled tokens but no people or roles, no external security audit, little adoption. ## Publishing and price | | SeeDropHost | Netlify | Vercel | Cloudflare | GitHub Pages | Surge | Render | Tiiny.host | |---|---|---|---|---|---|---|---|---| | Publish without an account | Yes, if the operator enables it: 24 h, claim code. [Limits](/docs/limits/) | Yes: drag-and-drop Drop, and CLI `--allow-anonymous` (claim within 1 h). [Source](https://docs.netlify.com/deploy/create-deploys/) | Yes: Vercel Drop and `vercel deploy --temporary`, expires, claim link. [Source](https://vercel.com/docs/deployments/deploy-without-logging-in) | Not found. [Source](https://developers.cloudflare.com/workers/static-assets/get-started/) | No: needs an account and a repository. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site) | First `surge publish` creates an account with email and password. [Source](https://surge.sh/docs/getting-started) | No. [Source](https://render.com/docs/static-sites) | No: free account required. [Source](https://tiiny.host/compare/) | | Free plan | 3 sites, 30 GB/month (soft limit), 100 leads/month. The limits, the usage metering and the billing by Pix and boleto are in the code; the hosted plan itself (**Coming soon**) opens with the first public server. Self-host: your server's cost. [Pricing](/pricing/) | 300 credits per month, hard limit (project stops when they run out). [Source](https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/) | Hobby: 100 GB fast transfer, 100 deploys/day, 200 projects. [Source](https://vercel.com/docs/plans/hobby) | Static asset requests free and unlimited. [Source](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) | Public repository on Free; 1 GB site; soft 100 GB/month. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits) | Free-plan limits not found. [Source](https://surge.sh/docs/getting-started) | Free static sites count against workspace bandwidth and pipeline minutes. [Source](https://render.com/docs/static-sites) | 1 project, 3 MB per upload, 5 000 visits and 5 GB per month. [Source](https://tiiny.host/compare/) | | Commercial use on the free tier | Allowed on every plan, Free included; it applies once the hosted plans open (**Coming soon**). [Pricing](/pricing/) | — | Hobby is non-commercial only. [Source](https://vercel.com/docs/plans/hobby) | — | Not for sales or SaaS sites. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits) | — | — | — | | Deploys on the free tier | Unlimited on every plan; deploys never count. [Pricing](/pricing/) | A production deploy uses 15 of the 300 monthly credits; previews and branch deploys use none. [Source](https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/) | Hobby: 100 deploys per day. [Source](https://vercel.com/docs/plans/hobby) | — | — | — | — | — | | When the free limit runs out | In the code: only a warning, never a block; every lead is kept, and creating a site beyond the plan is the one refusal. After a failed payment there are 30 days to pay with everything as it is, and then the site still stays live. [Pricing](/pricing/) | The project stops; unused credits do not roll over. [Source](https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/) | — | — | Bandwidth (100 GB/month) and builds (10/hour) are soft limits. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits) | — | — | — | | Price in reais / Pix | Fixed price in reais per account; Pix and boleto are in the code (a card also works on the payment page). The nota fiscal (**Coming soon**) and the hosted plans, which open with the first public server (**Coming soon**). [Pricing](/pricing/) | Prices in USD; Pix not found. [Source](https://www.netlify.com/pricing/) | Prices in USD; Pix not found. [Source](https://vercel.com/docs/plans/hobby) | Not found. | Not found. | Not found. | Not found. | Prices in USD; Pix not found. [Source](https://tiiny.host/compare/) | | Folder / .zip / single HTML | All three, one request each. [Quickstart](/docs/quickstart/) | Folder by drag-and-drop; zip via API. [Source](https://docs.netlify.com/deploy/create-deploys/) | File, folder or .zip in Drop; `vercel index.html`. [Source](https://vercel.com/docs/cli/deploy) | Direct Upload of a built folder (Pages). [Source](https://developers.cloudflare.com/pages/) | From a branch/folder or GitHub Actions. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site) | Folder or a single HTML. [Source](https://surge.sh/llms.txt) | Git repository and branch. [Source](https://render.com/docs/static-sites) | File or zip upload in the dashboard (not confirmed). [Source](https://tiiny.host/compare/) | | One-command CLI | `sdh deploy --site x` (from the repository; not on npm). [CLI](/docs/cli/) | `netlify deploy --prod`. [Source](https://docs.netlify.com/deploy/create-deploys/) | `vercel`. [Source](https://vercel.com/docs/cli/deploy) | `npx wrangler deploy` after creating the project. [Source](https://developers.cloudflare.com/workers/static-assets/get-started/) | No own CLI; `git push`. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site) | `surge publish`. [Source](https://surge.sh/docs/getting-started) | Not found. [Source](https://render.com/docs/static-sites) | Not found. [Source](https://tiiny.host/compare/) | ## Site features | | SeeDropHost | Netlify | Vercel | Cloudflare | GitHub Pages | Surge | Render | Tiiny.host | |---|---|---|---|---|---|---|---|---| | Custom domain + HTTPS | Domain after TXT verification; HTTPS by Caddy in the self-host install, only for verified names. [Self-host](/docs/self-host/) | Yes, automatic SSL on Free. [Source](https://www.netlify.com/pricing/personal-vs-free.md) | 50 domains per project on Hobby. [Source](https://vercel.com/docs/plans/hobby) | Pages: 100 domains per project. [Source](https://developers.cloudflare.com/pages/platform/limits/) | Custom domain yes. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) | Free custom domains, automatic SSL. [Source](https://surge.sh/docs/getting-started) | Yes, automatic TLS; 2 domains on Hobby. [Source](https://render.com/docs/static-sites) | From the paid Solo plan. [Source](https://tiiny.host/compare/) | | Forms without a backend | Yes: native or `data-netlify` markup, honeypot, link rule, rate limit, lead inbox. [Forms](/docs/forms/) | Yes, `data-netlify`; reCAPTCHA option. [Source](https://docs.netlify.com/manage/forms/setup/) | Not found. [Source](https://vercel.com/llms.txt) | Not found. [Source](https://developers.cloudflare.com/llms.txt) | Not found. [Source](https://docs.github.com/llms.txt) | Not found. [Source](https://surge.sh/llms.txt) | Not found. [Source](https://render.com/llms.txt) | Not as its own feature; email capture from the Pro plan. [Source](https://tiiny.host/compare/) | | Previews | Every deploy has its own URL; drafts are `noindex`. [API](/docs/api/) | Unlimited deploy previews. [Source](https://www.netlify.com/pricing/) | Preview environment. [Source](https://vercel.com/docs/instant-rollback) | URL per version (Workers). [Source](https://developers.cloudflare.com/workers/configuration/previews/) | Not found. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) | Not found. [Source](https://surge.sh/docs/getting-started) | Preview per pull request. [Source](https://render.com/docs/static-sites) | Not found. [Source](https://tiiny.host/compare/) | | Rollback | Publish any earlier deploy, no rebuild. [CLI](/docs/cli/) | Publish an older deploy, instant. [Source](https://docs.netlify.com/deploy/manage-deploys/manage-deploys-overview/) | Instant Rollback; on Hobby only to the previous deploy. [Source](https://vercel.com/docs/instant-rollback) | `wrangler rollback`, last 100 versions. [Source](https://developers.cloudflare.com/workers/configuration/versions-and-deployments/rollbacks/) | Not found (through git). [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) | `surge rollback`. [Source](https://surge.sh/docs/getting-started) | Exists; not confirmed for static sites. [Source](https://render.com/docs/rollbacks) | Not found. [Source](https://tiiny.host/compare/) | | `_redirects` / `_headers` files | Yes, plus `[[redirects]]` and `[[headers]]` from `netlify.toml`; no rule-count limit in code. [Format](/docs/redirects-headers/) | Yes (see docs index). [Source](https://docs.netlify.com/llms.txt) | — | Pages: 2 100 redirect rules, 100 header rules. [Source](https://developers.cloudflare.com/pages/platform/limits/) | — | — | — | — | | Build from Git | No: `[build] command` is not run; `[build] publish` is read. [Migrate](/docs/migrate-from-netlify/) | Yes (see docs index). [Source](https://docs.netlify.com/llms.txt) | Yes (see docs index). [Source](https://vercel.com/llms.txt) | Pages: 500 builds per month on Free. [Source](https://developers.cloudflare.com/pages/platform/limits/) | Branch or GitHub Actions; 10 builds/hour (soft). [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits) | — | Git repository and branch. [Source](https://render.com/docs/static-sites) | — | | Serverless functions | Off by default; with `SDH_FUNCTIONS=1`, one-file JS functions (10 s, 128 MB). Not isolated: single-owner servers only, and only the main account runs them. [Migrate](/docs/migrate-from-netlify/) | Yes (see docs index). [Source](https://docs.netlify.com/llms.txt) | Yes (see docs index). [Source](https://vercel.com/llms.txt) | Workers: 100 000 requests/day free. [Source](https://developers.cloudflare.com/workers/platform/limits/) | — | — | — | — | | CDN / presence in Brazil | No own CDN; served from your server, Cloudflare in front in the guide. [Self-host](/docs/self-host/) | PoP list not found. [Source](https://docs.netlify.com/llms.txt) | 126 PoPs in 94 cities; compute region São Paulo (`gru1`). [Source](https://vercel.com/docs/regions) | Network lists São Paulo, Rio de Janeiro, Brasília, Belo Horizonte, Porto Alegre. [Source](https://www.cloudflare.com/network/) | Not found. [Source](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages) | Not found. [Source](https://surge.sh/docs/getting-started) | Not found. [Source](https://render.com/docs/static-sites) | Not found. [Source](https://tiiny.host/compare/) | ## Agents and operations | | SeeDropHost | Netlify | Vercel | Cloudflare | GitHub Pages | Surge | Render | Tiiny.host | |---|---|---|---|---|---|---|---|---| | Official MCP server | Local (stdio) and remote (`/mcp`, Bearer token; OAuth **In progress**). Agent deploys are always drafts. [MCP](/docs/mcp/) | Remote and local. [Source](https://docs.netlify.com/build/build-with-ai/netlify-mcp-server/) | Remote, OAuth, approved clients; can deploy. [Source](https://vercel.com/docs/agent-resources/vercel-mcp) | Yes (whole API); Workers deploy not confirmed. [Source](https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/) | GitHub MCP; no Pages tool seen. [Source](https://github.com/github/github-mcp-server) | Not found. [Source](https://surge.sh/llms.txt) | Yes, OAuth or API key; creates static sites. [Source](https://render.com/docs/mcp-server) | Not found. [Source](https://tiiny.host/llms.txt) | | llms.txt / Markdown docs | Yes: [/llms.txt](/llms.txt), `.md` per page, [/openapi.json](/openapi.json) | Yes, `.md` for every page. [Source](https://docs.netlify.com/llms.txt) | Yes. [Source](https://vercel.com/llms.txt) | Yes, per product. [Source](https://developers.cloudflare.com/llms.txt) | Yes. [Source](https://docs.github.com/llms.txt) | Yes. [Source](https://surge.sh/llms.txt) | Yes. [Source](https://render.com/llms.txt) | 3 lines, no useful content. [Source](https://tiiny.host/llms.txt) | | Public status page | Not yet: arrives with the first public node. [Status](/status/) | Yes, 90-day uptime. [Source](https://www.netlifystatus.com/) | Yes, 90 days. [Source](https://www.vercel-status.com/) | Incident history; uptime figure not found. [Source](https://www.cloudflarestatus.com/history) | — | — | — | — | | URL-by-URL migration check | Yes: `sdh comparar` and MCP `compare_sites`, against a draft preview, including old `_redirects` addresses and `robots.txt`. [CLI](/docs/cli/) | Not found. [Source](https://docs.netlify.com/llms.txt) | Not found. [Source](https://vercel.com/llms.txt) | Not found. [Source](https://developers.cloudflare.com/llms.txt) | Not found. [Source](https://docs.github.com/llms.txt) | Not found. [Source](https://surge.sh/llms.txt) | Not found. [Source](https://render.com/llms.txt) | Not found. [Source](https://tiiny.host/compare/) | ## When to choose another service - **You want a hosted service today, with a free plan:** Cloudflare (unlimited static asset requests), GitHub Pages (public repositories; not for sales or SaaS), Netlify (300 credits per month) or Vercel Hobby (non-commercial use only). - **You share a server with other people and need functions:** functions here are not isolated. Use Netlify, Vercel, Cloudflare or Render. - **Next.js or anything that needs a build and server code:** Vercel, Netlify, Cloudflare or Render. - **A global edge network with presence in Brazil without running a server:** Cloudflare or Vercel. - **Your docs or project site already live in a GitHub repository:** GitHub Pages. ## When SeeDropHost fits - The site is already built (HTML, a zip, a folder) and you want it on **your own server**, with HTTPS and backups you control. - An **AI agent** publishes for you and you want every agent deploy to wait as a draft for a human to approve. - You are leaving a host and want to **prove, page by page,** that SEO and forms survive before switching DNS. - You need forms with a lead inbox, `_redirects` / `_headers` / `netlify.toml` compatibility and an audit log of who changed what. --- # SeeDropHost pricing: Free, Pro and Agency Checked against the code on 2026-10-31 (version 0.1.0). Three plans with a fixed monthly price in Brazilian reais (in US dollars outside Brazil), per account and not per person. Deploys are unlimited on all of them, and the free plan allows commercial use. > **The hosted plans are not open yet.** They open together with the first public SeeDropHost server; there is no date. Until then there is nothing to buy, and you can run SeeDropHost on your own server at no cost from us ([self-host guide](/docs/self-host/)). Items marked **Coming soon** do not exist in the code yet. Accounts with their own tokens, plan limits, usage metering, billing by Pix and boleto, the 30-day grace after a failed payment and the "take everything" export already do ([API](/docs/api/#billing)). ## Six promises 1. **Deploys never count.** Unlimited on every plan. Publish 100 times a day if you want. Check: the code has no deploy counter; the only deploy limits are size and file count per deploy ([limits](/docs/limits/)). 2. **Your site never stops over a limit.** This behaviour is already in the code: bandwidth or leads over the plan only produce a warning, never a block, and every lead is kept. Two months in a row: we ask you to change plan, with 14 days to do it. Payment failed: you have 30 days to pay with everything as it is; after that the site still stays live, and only creating a new site follows the Free limits. Nothing is deleted. The only refusal is creating one more site beyond the plan's site count, and nothing that is live is touched. Applies to verified sites on an account; anonymous drops still expire after 24 h. 3. **Commercial use on Free.** A shop, a company, a client's site: allowed on every plan, Free included. 4. **Fixed price in reais, per account.** Pix and boleto are already in the code, on the payment page of our provider (Asaas), where a card also works ([billing API](/docs/api/#billing), `sdh plano`, `sdh assinar`). No price per seat. Still to come: the nota fiscal (Brazilian service invoice) (**Coming soon**) and the hosted plans themselves, which open with the first public server (**Coming soon**). Outside Brazil: the same plans in US dollars (**Coming soon**). 5. **Migration with a report, on any plan.** `sdh comparar` checks your current site against the new one page by page — SEO and forms — before you switch DNS ([CLI](/docs/cli/), [migration guide](/docs/migrate-from-netlify/)). 6. **Free to leave.** The "take everything" export exists today: a zip with the site files (including `_redirects`, `_headers` and `netlify.toml` / `seedrop.toml`), the leads as `leads.csv` and the names of the variables. Values of variables never leave. Run `sdh exportar my-site`, use the button in the dashboard, call `GET /v1/sites/{site}/export` or the MCP tool `export_site` ([API](/docs/api/), [CLI](/docs/cli/)). ## Plans | | Free | Pro | Agency | |---|---|---|---| | Price | R$ 0, forever | R$ 29/month (R$ 290/year by Pix) | R$ 149/month (R$ 1,490/year) | | In US dollars (outside Brazil) (**Coming soon**) | US$ 0 | US$ 6/month | US$ 29/month | | Sites | 3 | 25 | 150 | | Deploys | Unlimited | Unlimited | Unlimited | | Custom domain + HTTPS | Every site | Every site | Every site | | Bandwidth per month (soft limit) | 30 GB | 300 GB | 2 TB | | Form leads per month | 100 | Unlimited | Unlimited | | Lead alerts | In the dashboard | Email + WhatsApp (**Coming soon**) | Email + WhatsApp, per client (**Coming soon**) | | SEO/GEO report on every deploy, migration check, local and remote MCP | Yes | Yes | Yes | | Automatic IndexNow | — | Yes (**Coming soon**) | Yes (**Coming soon**) | | Functions | — | Once they are isolated (**Coming soon**) | Once they are isolated (**Coming soon**) | | People on the account | 1 | Unlimited (**Coming soon**; today: several labelled tokens) | Unlimited (**Coming soon**; today: several labelled tokens) | | Dashboard with the agency's brand for the end client | — | — | Yes (**Coming soon**) | | Monthly SEO/GEO report per client | — | — | Yes (**Coming soon**) | | "Take everything" export | Yes | Yes | Yes | | Commercial use | Yes | Yes | Yes | Sites, bandwidth and leads are already counted in the code (`sdh uso`, `GET /v1/account/usage`). Going over a limit gives a warning, never a stop; the only refusal is creating a site beyond the plan, and nothing that is live is touched. ## Questions ### What happens if I go over a limit? You get a warning and your site stays live. If it happens two months in a row, we ask you to move to a bigger plan and give you 14 days to do it. Every lead is kept. If a payment fails, you have 30 days to pay with everything as it is; after that the site still stays live, and only creating a new site follows the Free limits. Nothing is deleted. This applies to verified sites on an account; anonymous drops still expire after 24 hours. ### Can I use the free plan for commercial work? Yes. A shop, a company site or a client's site is allowed on every plan, Free included. ### How do I pay? In Brazil: Pix or boleto, at a fixed price in reais per account, with no charge per person; a card also works on the payment page. This is already in the code. The nota fiscal (Brazilian service invoice) is coming soon. Outside Brazil: the same plans in US dollars, also coming soon. The hosted plans open together with the first public server; until then there is nothing to pay. ### What counts as bandwidth? The data sent to the visitors of your sites in the month: pages, images and files. Uploading a deploy and using the dashboard, API, CLI or MCP do not count. It is a soft limit: going over gives a warning, not a stop. For heavy video or large downloads, we recommend hosting the media elsewhere. ### Can I take everything with me if I leave? Yes. The "take everything" export exists today: run sdh exportar, use the button in the dashboard, call GET /v1/sites/{site}/export or use the MCP tool export_site. You get a zip with your site files (including _redirects, _headers and netlify.toml), your leads as CSV and the names of your variables; the values of variables never leave. SeeDropHost itself can also run on your own server.