# Getting started

> Set up your site with the Deploy to Cloudflare button and create the first admin.

Follow these steps to create a private copy of the starter, enter the secrets, add your own domain, set up [Turnstile](https://goodnight-web.dev/docs/glossary.md#turnstile), and create the first admin.

## What you need

- A Cloudflare account. The free plan is enough for a small website.
- A GitHub account.
- A domain name, if you want the site on your own domain. Decide this before you register a [passkey](https://goodnight-web.dev/docs/glossary.md#passkey) (see [Add your domain](#custom-domain)).

<Steps>

<Step>

### Press the Deploy to Cloudflare button

The button is in the README of the starter, the base of your site. It opens a Cloudflare setup form, which asks you to connect GitHub. The same form also asks for the secrets in the next step, so fill those in before you continue. Cloudflare then does the rest ([Cloudflare: Deploy buttons](https://developers.cloudflare.com/workers/platform/deploy-buttons/)):

- copies the starter into a new repository in your account
- creates the [D1](https://goodnight-web.dev/docs/glossary.md#d1) database
- builds and deploys the [Worker](https://goodnight-web.dev/docs/glossary.md#workers) with [Workers Builds](https://goodnight-web.dev/docs/glossary.md#workers-builds)

In the setup form, **tick "Create private Git repository"**. Your repository must be private; [Distribution and updates](https://goodnight-web.dev/docs/distribution.md#private-repository) explains why.

If the repository was created as public, open **Settings > General > Danger Zone > Change visibility** in GitHub and choose private. Until you do, the starter's visibility check workflow fails on every push.

</Step>

<Step>

### Fill in the setup form

The same setup form asks for the values below. Two of them are required: `APP_SECRET` and `ADMIN_EMAILS`. The secrets are private values stored with the Worker. You can also set or change them later in the Cloudflare dashboard, in the Worker's settings under variables and secrets.

| Name                 | Required             | What it is for                                                                                                                                                                                                  | How to make it                                                                         |
| -------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `APP_SECRET`         | Yes                  | Proves that you own the site when you create the first admin. The site also derives the key that signs admin sessions and the key that hashes IP addresses from it, so it stays in use after setup.             | A random string of 32 characters or more. The command is below.                        |
| `ADMIN_EMAILS`       | Yes                  | The addresses that may become and stay admins, separated by commas. Only one admin can be created for now: setup closes when the first passkey is registered ([Passkey sign-in](https://goodnight-web.dev/docs/how-it-works/passkeys.md)). | Type your own address.                                                                 |
| `TURNSTILE_SECRET`   | For the contact form | The Turnstile secret key.                                                                                                                                                                                       | Create a Turnstile widget in the Cloudflare dashboard and copy its secret key.         |
| `TURNSTILE_SITE_KEY` | For the contact form | The Turnstile site key. It is public, not a secret.                                                                                                                                                             | The same widget as the secret key.                                                     |
| `SLACK_WEBHOOK_URL`  | No                   | New-inquiry notices to Slack. They carry no names or form contents.                                                                                                                                             | Create an incoming webhook in Slack.                                                   |
| Project name         | Form field           | The name of the Worker. It is part of the `workers.dev` address.                                                                                                                                                | Pick a short lowercase name.                                                           |
| D1 database          | Form field           | Stores inquiries and admin accounts, including passkeys.                                                                                                                                                        | The form creates it; you only choose the name. The `deploy` script creates the tables. |

The table below shows when to enter each value, and what happens if you lose it or remove it.

| Name                 | When to enter it                                                                                                                                          | If it is lost or removed                                                                                                                                                                                                                                                                                                                                                                                                                        |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_SECRET`         | In the form. You type it once more at `/admin` to create the first admin.                                                                                 | Do not delete or change it after setup. Missing or too short: setup, sign-in, the admin screen, and the contact form answer 503. Changed: every admin is signed out and signs in again with a passkey, and new IP hashes no longer match old ones. Cloudflare does not show a secret again after you save it, so keep a copy in a password manager. You need the value again to [recover a lost passkey](https://goodnight-web.dev/docs/how-it-works/passkey-recovery.md). |
| `ADMIN_EMAILS`       | In the form.                                                                                                                                              | The Worker reads it on every request. An address you remove gets 403 at its next request. Setup stays closed while the list is empty.                                                                                                                                                                                                                                                                                                           |
| `TURNSTILE_SECRET`   | Later is fine. Leave it empty now and add it in [Set up Turnstile](https://goodnight-web.dev/docs/setup/turnstile.md).                                                               | Missing: the contact form shows "not available yet" and rejects submissions. Saved inquiries stay.                                                                                                                                                                                                                                                                                                                                              |
| `TURNSTILE_SITE_KEY` | Leave it empty in the form. After you create the widget, commit it to `wrangler.jsonc`. A value set only in the dashboard is replaced on the next deploy. | Empty: the same as a missing `TURNSTILE_SECRET`.                                                                                                                                                                                                                                                                                                                                                                                                |
| `SLACK_WEBHOOK_URL`  | Any time.                                                                                                                                                 | Missing: no notices. Inquiries are still saved and listed in the admin screen.                                                                                                                                                                                                                                                                                                                                                                  |
| Project name         | In the form.                                                                                                                                              | If you change `name` in `wrangler.jsonc` later, the next deploy creates a different Worker, without the secrets and domain of the old one.                                                                                                                                                                                                                                                                                                      |
| D1 database          | In the form.                                                                                                                                              | If you delete the database, you lose every inquiry and admin account. A new empty database needs setup and a new passkey again.                                                                                                                                                                                                                                                                                                                 |

The setup form does not generate these values for you. A value shorter than 32 characters counts as missing, and the features that use it stay off. So does a value that is clearly not random, such as one character repeated or one short block repeated; the setup screen of your site then names the secret to replace.

To make a random string that meets this rule (44 characters), run this in a terminal on your computer:

```sh
openssl rand -base64 32
```

Do not reuse one value for two secrets. Do not commit secrets to the repository.

The Worker derives two keys from `APP_SECRET` with [HKDF](https://goodnight-web.dev/docs/glossary.md#hkdf) in Web Crypto: one signs admin sessions and the other hashes IP addresses. Each purpose uses its own fixed label, so knowing one key does not reveal `APP_SECRET` or the other key. To set the keys yourself instead, see [Separate keys](#separate-keys).

</Step>

<Step>

### Check the first deploy

Open the site at the address that Workers Builds shows, and check that the pages load.

The database tables are already there, because the `deploy` script applies the D1 migrations before each deploy.

</Step>

<Step>

### Add your domain before you register a passkey [#custom-domain]

A passkey works only on the domain it was registered on (its WebAuthn relying party ID). A passkey registered on a `workers.dev` address stops working when you move the admin screen to your own domain.

Setup closes when the first passkey is registered, so you cannot register a new passkey on the new domain either. Reopening setup takes a manual change in the database ([When you cannot use or have lost your passkey](https://goodnight-web.dev/docs/how-it-works/passkey-recovery.md)), so add your domain before you create the first admin.

1. In the Cloudflare dashboard, add a custom domain to the Worker.
2. In `wrangler.jsonc`, change `"workers_dev": true` to `"workers_dev": false` and commit it. The deploy that follows turns the `workers.dev` address off, and the admin screen is then reachable only on your domain. Do not use the switch in the dashboard instead: every deploy applies `wrangler.jsonc` again and turns `workers.dev` back on. Preview URLs are already off in the starter (`"preview_urls": false`).

</Step>

<Step>

### Set up Turnstile

Create a Turnstile widget. For its hostname, use the domain that serves your site. Put its site key in `wrangler.jsonc` (`TURNSTILE_SITE_KEY`) and its secret key in the `TURNSTILE_SECRET` secret.

Until you do, the contact form shows "The contact form is not available yet" and rejects submissions. Follow [Set up Turnstile](https://goodnight-web.dev/docs/setup/turnstile.md).

</Step>

<Step>

### Create the first admin

The admin screen works only over `https://`.

1. Open `https://<your domain>/admin/`. No admin exists yet, so the setup screen (`/admin/setup`) opens.
2. Enter the value of `APP_SECRET` as the **Setup token** and an address from `ADMIN_EMAILS` as the **Email address**, then select **Continue**.
3. Select **Register a passkey** and create the passkey with your browser or password manager. Save it where it syncs across your devices, such as iCloud Keychain, Google Password Manager, or a password manager app. The admin screen has no page to add a second passkey yet ([Passkey sign-in](https://goodnight-web.dev/docs/how-it-works/passkeys.md)). If your browser or computer cannot keep a passkey, or you lose it, see [When you cannot use or have lost your passkey](https://goodnight-web.dev/docs/how-it-works/passkey-recovery.md).

<AdminScreen
  name='setup'
  alt='The first-admin setup screen with the setup token and an email address filled in.'
  caption='Step 1 of setup. The next step registers a passkey.'
/>

After the first passkey is registered, setup closes and the setup screen no longer works. Keep the `APP_SECRET` secret: the site still uses it for sign-in and IP hashes. If you set `SETUP_TOKEN` (see [Separate keys](#separate-keys)), delete it now. [First admin setup](https://goodnight-web.dev/docs/how-it-works/first-admin.md) explains the rules.

</Step>

<Step>

### Optional: put Cloudflare Access in front of the admin screen

Cloudflare Access adds a second sign-in, for example a one-time code sent by email. It is free for up to 50 users.

It is recommended but not required. The admin screen always requires a passkey sign-in, with or without Access.

Put the Access application on these paths:

- `/admin`, `/admin/*`
- `/api/admin/*`
- `/api/setup`
- `/api/auth/*`

Keep `/api/contact` public. Visitors send the contact form there.

To have the Worker check the Access token as well, add two variables:

- `ACCESS_AUD`: the application's **Application Audience (AUD) Tag**
- `ACCESS_TEAM_DOMAIN`: for example `your-team.cloudflareaccess.com`

Set both or neither. With only one, the admin paths return 503. See [Security model](https://goodnight-web.dev/docs/security.md#cloudflare-access).

</Step>

<Step>

### Edit your site

The site has Japanese and English pages. Edit both:

- Company details and the three theme colors: `site.config.ts`
- Pages: `src/pages`
- Posts: `src/content`

The contact form fields are not in the code. Change them in the admin screen ([Customizing the contact form](https://goodnight-web.dev/docs/contact-form.md)).

Have a professional review the privacy policy template before you publish. [Privacy notes](https://goodnight-web.dev/docs/privacy.md) lists what the template covers.

</Step>

</Steps>

## Separate keys [#separate-keys]

`APP_SECRET` is the default and is enough for a site. If you prefer to manage each key on its own, you can also set these secrets in the Cloudflare dashboard or with `npx wrangler secret put`. The Deploy to Cloudflare form does not ask for them.

| Name                 | What it replaces                                                                                                            |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `SETUP_TOKEN`        | The value you enter on the setup screen. When it is set, `APP_SECRET` is not accepted there. You can delete it after setup. |
| `BETTER_AUTH_SECRET` | The key that signs admin sessions.                                                                                          |
| `IP_HASH_SECRET`     | The key that hashes IP addresses.                                                                                           |

Each must be a random string of 32 characters or more. A value you set is used instead of the one derived from `APP_SECRET`, and a value that is too short turns its features off instead of falling back to `APP_SECRET`. Sites that already set these three secrets keep working as before.

Adding or removing `BETTER_AUTH_SECRET` or `IP_HASH_SECRET` on a running site changes that key, with the same effects as changing `APP_SECRET`.

## Without the Deploy button

You can also create a project with `npm create cloudflare` (C3). C3 only creates a local project. You create the GitHub repository yourself, so **create it as private**.
