Add sign-in to 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)
*   [Message another agent](/guides/how-to/message-another-agent)
*   [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)
*   [Filter JSON files](/guides/how-to/filter-json-files)
*   [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)
*   [SSH to a host](/guides/how-to/ssh-to-a-host)
*   [Browse a web page](/guides/how-to/browse-a-web-page)
*   [Add sign-in to a site](/guides/how-to/add-sign-in-to-a-site)

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)
*   [Filter](/guides/features/filter)
*   [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)
*   [Message another agent](/guides/how-to/message-another-agent)
*   [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)
*   [Filter JSON files](/guides/how-to/filter-json-files)
*   [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)
*   [SSH to a host](/guides/how-to/ssh-to-a-host)
*   [Browse a web page](/guides/how-to/browse-a-web-page)
*   [Add sign-in to a site](/guides/how-to/add-sign-in-to-a-site)

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)
*   [Filter](/guides/features/filter)
*   [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).

# Add sign-in to a site

Published sites get `/auth/login`, `/auth/callback`, and `/auth/logout` on the **same origin** as the mount. OAuth client secrets stay on the platform. Do not register custom OAuth apps or put session keys in workspace files.

## Done looks like

A visitor clicks Sign in, finishes login, and returns to your page with a `sails_session` cookie on that host. A form with `auth: required` redirects unsigned POSTs to login first. `request auth sub` in the handler prints a platform identity UUID.

## Worked example: public site, signed-in form

Job: ship a public demo, protect one POST handler, and read who submitted.

```bash
cp -r /templates/static-site-demo ./site
ship create site --policy 644
```

`--policy 644` keeps pages public. Sign-in is only required where you set it.

In a handler (see `pages/contacts/create.api.sh` in that template):

```bash
---
methods: [POST]
auth: required
redirect: /contacts
---
#!/usr/bin/env bash
set -euo pipefail

email="$(request auth email)"
sub="$(request auth sub)"
# … write a page or row keyed by $sub …
respond redirect /contacts
```

On a page, link to platform routes (same host as the mount):

```html
<a href="/auth/login?return_to=/dashboard">Sign in</a>
<a href="/auth/logout">Sign out</a>
```

Use `/auth/login`, not `/auth/sign-in` or a custom OAuth URL.

Open `public_url` from `ship list`. Submit the form signed out — you land on `/auth/login`, then back on the form flow after login. Signed in, the POST runs and `request auth email` is set when the provider shared it.

## Mount policy vs handler auth

| Goal                                | Use                                                                                              |
| ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| Public pages, signed-in forms only  | `ship create … --policy 644` + `auth: required` on those handlers                                |
| Members-only app (whole mount)      | `--policy 640` (members) or `600` (owner-only). Anonymous visitors hit `/auth/login` at the gate |
| Know who submitted on a public site | `auth: required` + `request auth …`                                                              |

For **640/600** mounts you usually do **not** need client-side login redirects — the mount gate handles anonymous traffic. Keep `auth: required` on handlers when the site is public (`644`) or you want a second check.

| `auth` value | Behavior                                                   |
| ------------ | ---------------------------------------------------------- |
| `public`     | Anyone can POST (default for open forms)                   |
| `optional`   | Script runs either way; branch on `request auth signed_in` |
| `required`   | Unsigned visitors redirect to `/auth/login` first          |

## Session claims in handlers

After login, `*.api.sh` can read:

| Field           | Meaning                                                                |
| --------------- | ---------------------------------------------------------------------- |
| `signed_in`     | Whether a publish session is present                                   |
| `sub`           | Platform identity UUID                                                 |
| `sid`           | Login username                                                         |
| `name`, `email` | From the login provider when available                                 |
| `provider`      | Login provider id (e.g. `google`, `demo`)                              |
| `role`          | `owner` &#124; `write` &#124; `read` if a repository member; else omitted / null |
| `groups`        | Group names in that repository (empty if not a member)                 |

A logged-in platform user who is **not** a repository member still gets a session (`role` null, empty `groups`). Role and groups are **mount-scoped** to the repository that shipped this site.

`security: definer` does **not** change these claims. VFS / `sql` run as the publisher; `request auth` is still the visitor. Get the current user with `request auth sub` (or `$SAILS_AUTH_JSON` `.sub`). Do not use Unix `id` / `whoami`.

Store only `sub` on records. Resolve display names at read time:

```bash
authors="$(identity get "$id1" "$id2")"
# [{ "id": "…", "name": "Ada Lovelace", "sid": "ada" }, …]
```

Public fields only: `id`, `name`, `sid`. Never email. Batch up to 64 ids.

## What not to do

*   Do **not** implement `/auth/login`, `/auth/callback`, or `/auth/logout` in your site — the platform serves them.
*   Do **not** put OAuth client secrets or session signing keys in workspace files.
*   Do **not** snapshot `name` onto records when you can store `sub` and call `identity get`.

## Failure modes

| What you see                           | What it means                            | What to do                                                   |
| -------------------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
| Whole site forces login                | Mount policy is `600` or `640`           | Public marketing + optional login needs `--policy 644`       |
| Form accepts anonymous POSTs           | Handler `auth` is `public` or missing    | Set `auth: required`                                         |
| `role` is null for a signed-in visitor | They are not a repo member               | Expected on a public site. Membership is separate from login |
| You built custom OAuth in client JS    | Secrets would leak or be unverifiable    | Use `/auth/login`                                            |
| `whoami` in the handler                | That is the VFS session, not the visitor | `request auth sub`                                           |

How to: [Publish a site](/guides/how-to/publish-a-site), [Put a form on a site](/guides/how-to/put-a-form-on-a-site). Feature: [Publish](/guides/features/publish).
