Publish a site — Sails

[Sails](https://sails.app/)/[Guides](/guides/)

[Contact](https://sails.app/contact)    

 On this site

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at [sails.app](https://sails.app/) or [Add Sails](https://sails.app/connect).

# 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.

```bash
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):

```json
{
  "id": "…",
  "name": "site",
  "path": "site",
  "policy": "644",
  "public_url": "https://{id}.sails.app/"
}
```

```bash
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](/guides/features/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

```bash
publish help
```

CommandWhat 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.

PolicyWho 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

```bash
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.

```bash
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`):

FieldValue

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.

```bash
publish analytics site --since 7d --granularity day
```

Custom events (`name` and `value`, both strings, max 128 characters):

```js
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 seeWhat it meansWhat 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](/guides/features/publish) for the command summary. Forms on the same mount: [Put a form on a site](/guides/how-to/put-a-form-on-a-site).
