> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fetchski-attract.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Key in the database

> Keep the site's API key and signing secret in a Lucid table, sealed under APP_KEY, stored by one Ace command.

Instead of `.env`, keep the key in your app's database, sealed, and change it with one Ace command:
no redeploy, no edit to the environment. That suits an app where the key is rotated now and then, or
where the person who holds the key isn't the person who deploys.

> [card] Sealed under APP\_KEY
>
> AdonisJS's encryption service seals each value, tied to this table, so a value moved to another
> column won't open.

> [card] Stored by Ace
>
> `node ace attract:store-key` asks for the key, checks the site accepts it, then stores it.

> [card] Same calls
>
> The package builds the client from the row; `@inject()` and every call stay as they were.

**1. Set up Lucid**

The table lives in your app's database. A starter kit has Lucid already; otherwise:

```bash Terminal theme={"dark"}
node ace add @adonisjs/lucid
```

**2. Configure the package for the database**

```bash Terminal theme={"dark"}
node ace configure @fetchski/attract-cms-adonis --database
```

It writes `config/attract.ts` with `sitesFrom: 'database'`, the `attract_api_keys` migration, and
the provider and commands in `adonisrc.ts`. Nothing goes in `.env`: the key needs only the
`APP_KEY` every AdonisJS app has.

```ts config/attract.ts theme={"dark"}
import { defineConfig } from '@fetchski/attract-cms-adonis'

/**
 * The Fetchski Attract sites this app calls. The container holds SiteClient
 * (the default site's client) and SiteClients (every site, by slug):
 * @inject() either one.
 */
const attractConfig = defineConfig({
  // The keys live in attract_api_keys, sealed under APP_KEY:
  // node ace attract:store-key stores one, attract:forget-key deletes it.
  sitesFrom: 'database',

  // Milliseconds per attempt, and extra attempts for a call safe to repeat.
  timeout: 15_000,
  retries: 2,

  // One debug line per call on the app's logger (never a header or body).
  logCalls: true,
})

export default attractConfig
```

> [!NOTE]
>
> Switching an app that already keeps the key in `.env`? Add `--force`, or the command keeps the
> `config/attract.ts` you have (and says so). Once the key is stored, remove the
> `FETCHSKI_ATTRACT_*` lines from `.env`, `.env.example` and `start/env.ts`.

**3. Create the table**

```bash Terminal theme={"dark"}
node ace migration:run
```

One row per site: its slug (the `acme` in `fsk_acme_…`), its URL, the token and signing secret
(both sealed), and the key's signing mode.

**4. Store the key**

In the site's CMS, **API Keys → New API Key** shows the key, its signing secret and (on its
**Security** tab) its signing mode, once. Keep the page open, then:

```bash Terminal theme={"dark"}
node ace attract:store-key --url=https://acme.example
```

```text Terminal theme={"dark"}
❯ The API key (fsk_…) · ********************************
❯ Its signing mode, as the key's Security tab shows · Token + signature on writes
❯ Its signing secret (fss_…) · ********************************
[ success ] Stored the API key for acme, encrypted, and the site accepted it.
```

The key and its secret are asked hidden as you type: leave them out of the command, so they never
land in your shell history. A token-only key isn't asked for a secret.

It builds a client first (so a malformed key or URL fails before anything is sent), makes one call
to prove the site accepts the key, stores it, then runs [`attract:check`](https://docs.fetchski-attract.com/adonisjs/ace-commands#check-the-site).
A refused key stores nothing and exits `1`.

> [!NOTE]
>
> The proving call reads the site's locales, which any key may do, so the key's scopes don't
> matter. The site still checks the token, the key's network allowlist and, for a key that signs
> every request, its signature: a refusal of any of them stores nothing. A key that signs only
> writes has its secret checked at its first write, since a read is never signed.

The default site is the table's one row. Store a second site's key the same way and `SiteClient`
needs to know which is the default: set `default: 'acme'` in `config/attract.ts`, or take
`SiteClients` and ask for each by slug ([Many sites](https://docs.fetchski-attract.com/adonisjs/configuration.md#many-sites-by-slug)).

## Replace or forget the key

> [tab] Replace it
>
> Run `node ace attract:store-key` again with the new key: it replaces the site's row, so the old key
> and secret are gone the moment the new ones are stored (it never opens the old row, so this works
> even after `APP_KEY` changed). A running server keeps the `SiteClient` it built: restart it to pick
> up the new key, or take `SiteClients` and call `forSite()` for each job, which reads the table each
> time.

> [tab] Forget it
>
> ```bash Terminal theme={"dark"}
> node ace attract:forget-key acme
> ```
>
> It asks first (`--force` doesn't), and the slug can be left out while the table holds one key.
>
> > [!WARNING]
> >
> > Deleting the row doesn't switch the key off: it keeps working on the site until you revoke it
> > there too, in the CMS under **API Keys**.

> [tab] Change APP\_KEY
>
> `config/encryption.ts` takes a list of keys: the first encrypts, every one decrypts. Put the new
> key in `APP_KEY`, the old one in `OLD_APP_KEY` (declared in `start/env.ts` as
> `Env.schema.secret()`), and list both until the row has been stored again:
>
> ```ts config/encryption.ts theme={"dark"}
> gcm: drivers.aes256gcm({
>   keys: [env.get('APP_KEY'), env.get('OLD_APP_KEY')],
>   id: 'gcm',
> }),
> ```
>
> Then `node ace attract:store-key` once (sealed under the new key), and drop `OLD_APP_KEY`. A row
> no listed key opens throws, naming `attract:store-key`. Losing every key that sealed the row loses
> the stored key: back `APP_KEY` up with your other secrets.

## In tests

Tests fake the client ([Testing](https://docs.fetchski-attract.com/adonisjs/testing.md)): `fakeAttract()` replaces it whether the key is
in `.env` or the table, so a test needs no row and no key. The package's own tests check the sealing:
a stored row holds no part of the key in the clear, and reads back whole.

> [card] [Next: calling the API](https://docs.fetchski-attract.com/adonisjs/calling-the-api.md)
>
> Controllers, routes, validation, uploads and errors.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.