Skip to main content
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.

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.

Stored by Ace

node ace attract:store-key asks for the key, checks the site accepts it, then stores it.

Same calls

The package builds the client from the row; @inject() and every call stay as they were.

Set up Lucid

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

Configure the package for the database

Terminal
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.
config/attract.ts
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.

Create the table

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

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:
Terminal
Terminal
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. A refused key stores nothing and exits 1.
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).

Replace or forget the key

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.

In tests

Tests fake the client (Testing): 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.

Next: calling the API

Controllers, routes, validation, uploads and errors.