# Your first account

> One CLI command creates the first admin. What the store is, why sign-up cannot make itself an admin, and how the throttles behave when you get a password wrong.

Source: https://editor.astrocraftthemes.com/docs/first-account/

Everything under `/admin/` is behind a server-side guard, so the first thing a fresh install needs is an account. There is no environment variable to set and no database to provision — the store is a gitignored `.accounts/` directory at the repository root, and one command writes to it.

## Create it

```bash
node --experimental-strip-types src/admin/js/accounts.cli.ts you@example.com "Your Name"
```

It asks for the password on **stdin** — never as an argument, which would land in your shell history — applies the same rules the sign-up form does, and hashes with Argon2id at OWASP's parameters (`m=19456 t=2 p=1`, about 383 ms on real hardware). It prints where the record went.

The third argument is the role, and it defaults to `admin`:

```bash
node --experimental-strip-types src/admin/js/accounts.cli.ts \
  someone@example.com "Someone Else" editor
```

Run it again for an address that already exists and it replaces that account's password, name and role — which is also how you reset a password you have lost.

> This command must run under plain `node`, which resolves no tsconfig paths. That is a real constraint the codebase enforces with a test that actually runs the CLI as a subprocess: v1.0.0 shipped one aliased import inside it and every fresh install was locked out of its own CMS. If you ever modify anything reachable from this file, keep it alias-free.

## Then sign in

Start the dev server, open `/admin/`, and sign in with that address. The password floor is **12 characters** — above NIST's eight, and matched to the strength meter, which tops out at the same number.

## The three roles

| Role     | Can                                                                                                                                                                      |
| :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin`  | Publish and approve changes · manage the team and invites · change settings and field overrides · connect and disconnect the project · everything a client editor can do |
| `editor` | Write and edit entries · upload media and write alt text · submit changes for review · preview the site. **Cannot** publish, approve or manage the team                  |
| `none`   | Nothing. Sign-in refuses at the door                                                                                                                                     |

`none` is a real, selectable state rather than the absence of one — taking access away is a role change, not a deletion, so the person's account and history stay intact.

A role change lands on the demoted person's **next request**, not at their next sign-in. The guard re-reads the account from the store on every guarded request rather than trusting the session payload, which costs about 0.45 ms and means one refusal covers no session, deleted account, demoted to `none`, and a stale session all at once.

## Why sign-up cannot let itself in

`/admin/signup/` is public, and anyone who reaches it can create an account. That account gets `role: "none"`, which sign-in refuses until an admin grants access.

This is the whole design. A self-serve sign-up that granted itself access would hand a fresh deployment to whoever loaded the page first. It is also why the CLI exists: on a fresh install, sign-up is not a route to an admin account, and there is no other way in.

**Sign-up also needs mail configured.** Without `RESEND_API_KEY` and `MAIL_FROM`, the screen answers _"we can't create accounts right now, please try again later"_ — which is permanent rather than transient, because confirming a mailed link is how the address is proved. Use the CLI for the first account either way; it is the documented path. See [Environment variables](/docs/environment/).

## Inviting everybody else

Once you are in as an admin, `/admin/team/` is the normal route: an invite mints a token, stores it server-side, mails the link, and hands it back to the modal so you can copy it. The invited person sets their own password on arrival and lands with the role you chose. Full detail in [Team, roles and invites](/docs/team/).

Both token routes spend the token on **POST**, never on the GET that loads the page — a mail scanner following the link used to burn the invite before its recipient saw it.

## The throttles

Three separate scopes, and they behave differently on purpose.

**Sign-in** locks out after 10 attempts in 15 minutes, for 4 minutes. A miss pays for a password hash too, so an unknown address and a wrong password cost the same amount of time and neither can be used to enumerate the other.

**Sign-up** is a separate scope from sign-in. If it were not, anyone could lock an address out of its own account through the sign-up form.

**Sign-in also sheds a burst.** Past four requests in flight, the endpoint answers 429 with a real `Retry-After` and the screen draws its existing countdown. The hash is synchronous and single-threaded, so this is about the check-then-act window rather than about CPU: a simultaneous burst would otherwise all pass the lockout check before any of them recorded a failure.

## Where the store lives

`.accounts/`, gitignored. Deliberately not under `.astro/` or `node_modules/.astro/`, because losing it deletes the only account with access to your CMS — and `rm -rf node_modules` is a thing people do on a Tuesday.

### It is relative to the working directory, not to your project

This is the single most likely thing to bite you, and it is invisible when it does.

The path is the literal string `.accounts`, resolved by the storage driver against **`process.cwd()`**. It lands at your repository root only because that is where you normally start the server. Start it from anywhere else — or run the accounts CLI from anywhere else — and the CMS reads and writes `.accounts/` in _that_ directory instead.

Nothing warns you. An empty store looks exactly like a fresh install, which means the sign-in you expected to work instead re-opens first-admin sign-up to whoever loads the page.

So run both from the project root:

```bash
cd ~/projects/my-site
node --experimental-strip-types src/admin/js/accounts.cli.ts you@example.com "Your Name"
pnpm dev
```

The CLI prints where the record went. If that path is not the `.accounts/` beside your `package.json`, your cwd is wrong — that line is the check, and it is worth reading rather than skipping.

The same applies to the deployed standalone server, which is why the container starts inside its checkout. It also disappears entirely the moment the store is a database or a KV binding, because neither has a path to resolve.

It is an [unstorage](https://unstorage.unjs.io) namespace, so the same drivers Astro uses for sessions — Cloudflare KV, Vercel KV, Netlify Blobs, Redis, S3 — drop in with one line in `src/admin/js/accounts.ts`. For the destination most people asking that question actually mean, a real database, there is no line to change at all: see [Bring a database](/docs/database/).

Every store key is a SHA-256 hex digest of the value, which keeps email addresses off disk and sidesteps driver character-set limits.

## Troubleshooting

**`ERR_MODULE_NOT_FOUND` running the CLI.** You are on v1.0.0. That release shipped an aliased import in the one file that must not have one; 1.0.1 is the fix and the reason to take it.

**Sign-in says the account has no access.** The role is `none`. Either you signed up through the form (which is what it does) or an admin demoted you. An admin can promote from `/admin/team/`.

**Sign-up says accounts cannot be created right now.** Mail is not configured. That message is permanent, not transient.

**You are locked out with no admin.** Run the CLI again for your own address with no third argument; it replaces the role with `admin` along with the password.

**Sign-in does not recognise an account you definitely created, or the CMS offers first-admin sign-up again.** Your process's working directory is not the project root, so `.accounts/` resolved somewhere else and the store it found is empty. See _It is relative to the working directory_ above. Check for a stray `.accounts/` in whatever directory you started from.

**The store vanished after a redeploy.** `.accounts/` lives inside the checkout. If your container's volume does not hold the checkout, or you did not attach one, every redeploy is a fresh install. [Deployment](/docs/deployment/) covers the volume, and [Bring a database](/docs/database/) covers making the machine disposable instead.