Publish a site
publish create binds one folder to one URL. You run it once. Autosave redeploys that same URL on every checkpoint. There is no build command.
A directory with sails.site.json at the mount root compiles as a static site (.page.html / .page.md). Any other folder is served as files. Tailwind CSS and TypeScript/TSX are transpiled on publish.
Done looks like
publish list shows a row whose public_url you can open. For a public site that URL returns 200 without sign-in. After you edit a page, the same URL shows the change. You did not create a second mount.
Worked example: public marketing site
Job: copy a template, put it on a public URL, confirm the URL, then ship a copy change without publishing again.
cp -r /templates/static-site-demo ./site
publish create site --policy 644
<name> is required and unique in the repo. When --path is omitted, the folder defaults to <name>, so this publishes ./site as mount site.
--policy 644 is public web. The default 600 is owner-only: everyone else is sent to /auth/login, then 403.
Expected output is JSON. Shape (not exact ids):
{
"id": "…",
"name": "site",
"path": "site",
"policy": "644",
"public_url": "https://{id}.sails.app/"
}
publish list
Open public_url. That is the preview host. Then edit a page under site/pages/ and stop. Do not run publish create again. Do not run repo save as a ritual. Autosave already checkpointed; the mount tracks branch head.
See Static sites for the folder layout.
Who can publish
| Actor | create / update / delete | --hostname / publish verify |
|---|---|---|
| Repository owner | Any path, including repo root | Yes |
| write member / agent | Non-root path they have Unix write on (and inside an agent jail) | No |
| read member | No | No |
Agents can publish a preview URL for a folder they can write. Binding a custom hostname stays owner-only.
Flags
publish help
| Command | What it does |
|---|---|
publish create <name> [--path PATH] [--branch name] [--hostname host] [--policy MODE] [--mcp] | Create the mount once. Path defaults to <name>. |
publish list | JSON of mounts, including public_url |
publish update <id> [--name NAME] [--policy MODE] | Rename or change policy. Cannot change hostname. |
publish verify <id> | DNS ownership check for a custom domain |
publish analytics <id-or-name> [--since 7d] … | Page views and custom events (any member can read) |
publish delete <id> | Permanently kills the URL. Do not use this to “refresh” a link. |
--policy gates HTTP access to the whole mount (static files and *.api.sh). It does not rewrite per-file modes in the workspace.
| Policy | Who can load the URL |
|---|---|
| 644 | Anyone |
| 640 | Owner or default-group members (others sign in, then 403) |
| 600 | Owner only (default) |
--mcp turns on Streamable HTTP MCP at /mcp for *.mcp.sh on that mount.
Custom hostname
Platform hosts (*.sails.app, *.localtest.me, *.localhost) always include the org slug. publish create crm --hostname crm binds {org}-crm.sails.app, not crm.sails.app. Those hosts are auto-verified.
Custom domains do not get the org prefix. The first host on a new domain needs a DNS TXT. After that, this org can publish further hosts on that domain without another TXT.
--hostname host/prefix (example: {org}.sails.app/admin) mounts under a path. The host root must already be published by this repository. You cannot hang a prefix on another repo’s host.
publish update cannot change --hostname. To move a custom domain, delete the old mount and publish create the new one. Keep at least one verified host on the domain if you want to skip a new TXT.
Path on a host you already publish
publish create guides --path sites/sails-guides --hostname sails.app/guides --policy 644
Publish the host root first, then the prefix. sails.app/guides is a path on a host this repo already owns.
Custom domain TXT
Do not invent a TXT value. Sails prints a one-time token.
publish create site --path ./site --hostname example.com --policy 644
publish list
publish verify <id>
Until verify succeeds, public_url is still {published-path-id}.sails.app. hostname already shows example.com. Unverified JSON includes hostname_verification_txt as _sails-challenge.example.com = <token>. Copy the token (the part after =).
DNS (zone example.com):
| Field | Value |
|---|---|
| Type | TXT |
| Name / host | _sails-challenge |
| Value | the token only |
Then point the hostname at Sails: CNAME example.com → cname.sails.app. Apex domains often cannot CNAME; use ALIAS / ANAME / CNAME-flattening. publish verify checks ownership only — it does not wait for the CNAME.
After verify, public_url becomes https://example.com/ and hostname_verification_txt is omitted.
Analytics
HTML page views are recorded automatically. Unique visitors use a first-party sails_vid cookie. Known crawlers are excluded from headline counts.
publish analytics site --since 7d --granularity day
Custom events (name and value, both strings, max 128 characters):
navigator.sendBeacon('/sails/analytics/events', JSON.stringify({ name: 'signup', value: 'pro' }))
On a path-prefixed URL, POST /prefix/sails/analytics/events or POST /sails/analytics/events with a same-origin Referer. Do not count that POST as a page view.
--visitor / --exclude-visitor filter by sails_vid. --visitors-limit 0 omits the visitor list (use that on a public dashboard). In a published *.api.sh, security: definer queries as the publisher; security: invoker needs a signed-in member. publish create / update / delete / list / verify stay blocked inside published scripts.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
Visitors hit /auth/login then 403 | Policy is 600 or 640 | Public sites need --policy 644. Members-only is 640. |
Second publish create for the same folder | You tried to mint a “fresh” URL | Keep the first mount. publish list is the URL. |
publish delete then a new URL | You tore down the old links | Only delete when the URL should die for good |
Agent cannot --hostname | Custom hosts are owner-only | Owner binds the host; agent can still publish a preview path |
publish create denied for repo root | Write members / agents cannot publish / | Publish a subfolder they can write |
Custom domain still shows *.sails.app | TXT not verified | Copy hostname_verification_txt, add the TXT, publish verify <id> |
| Prefix URL 404s on another repo’s host | Prefix only on a host this repo publishes | Publish this repo’s host root first |
You ran repo save “to deploy” | Unnecessary | Autosave already updated the mount |
See Publish for the command summary. Forms on the same mount: Put a form on a site.