Skip to main content
A single site needs only .env. The configure command writes it for you:
Terminal
It adds these lines:
.env
string
required
The site’s slug: the acme in its key, fsk_acme_…, which the command reads from the key. A key filed under another slug is refused, naming the right one.
string
required
The site’s public origin: https:// and the host it answers on, with no path, query or credentials. A redirect is never followed; plain http:// reaches only localhost or a private service name.
string
required
fsk_<site>_<key id>_<secret>, as the site showed it. An empty value is refused, never taken as the public.
string
fss_…, when the key signs. With a secret, the client signs every request it can, uploads by their SHA-256. Signing requests lists which requests must be signed.
Each one gets a rule in start/env.ts, so a missing key or a mistyped URL stops the app at boot, and the key and secret are Secrets that print as [redacted]:
start/env.ts
tld: false lets a local site such as http://localhost:4321 through.

Or keep the key in your database

The key and signing secret sealed in a table, replaced without touching .env.

The config file

The command also writes config/attract.ts. Every option is here, with what it starts as:
config/attract.ts
defineConfig checks the file as it loads, so a mistake (a default no site is filed under, a negative timeout) stops the app at boot with one message naming the fix.
Leave apiKey out only for a site you read as the public. An apiKey that is there but empty (an unset variable) is refused, so a key that was meant to be there never quietly turns the client into the public’s.

Ask for it

In a controller, service, listener or queue job, take the client in the constructor:
app/controllers/blog_posts_controller.ts
The client is built the first time something asks for it, and the same one is handed out after. Anywhere else, ask the container: const site = await app.container.make(SiteClient) (with import app from '@adonisjs/core/services/app').
Import SiteClient as a value, never import type: @inject() reads the class at runtime, and a type-only import hands the constructor undefined.

The default site

SiteClient reaches the default

The site default names (FETCHSKI_ATTRACT_SITE), or, when that is unset, the one site configured (with the key in the database, the table’s one row).

SiteClients reaches any site

await sites.forSite('globex'): a client for that site, made on the spot (cheap: nothing is sent).

No default, several sites: a clear error

No default site among acme, globex: set default in config/attract.ts (FETCHSKI_ATTRACT_SITE), or take SiteClients and call forSite('<slug>').

Many sites, by slug

List each further site under its slug, beside the env one:
config/attract.ts
Declare each new key in start/env.ts as Env.schema.secret(). Then take the SiteClients directory and ask it for each site by its slug:
app/services/sites_report.ts
SiteClient keeps reaching the default site. A slug nothing lists throws UnknownSite; with the keys in the database, every row is a site (Key in the database).

Give tests a placeholder

Tests boot the app, so start/env.ts checks the variables there too. Tests fake the client (Testing) and never call the site, so a placeholder does:
.env.test

Keys stay secret

Secret until the client

Env.schema.secret() values print as [redacted], in logs and in node ace repl. Only the package releases them, into the client.

Never in the SDK's output

No error, log line or dump of the client holds a key (Keeping keys secret).

Next: key in the database

The key sealed under APP_KEY, stored with one Ace command.