Encrypted at rest
AES-256-GCM from
node:crypto, under an encryption key that never sits in the database.Replaced in seconds
Store a new key from the CMS and the old one is gone from your app.
Same calls
site.blogPosts().listBlogs() and every other call work as before, signed as the key asks.What you’ll build
The SDK has no database of its own, so this guide adds a few small files to your app. Each one is complete: copy it as shown.
Using another database or an ORM? Only
src/db.ts and the two queries in src/attract.ts change: see
Other databases.
Before you start
The SDK is installed
From your own site, with any of its API keys: see Installation.
Your app has a database
The one your app already uses. This guide’s code is for PostgreSQL through The guide’s script runs with
pg; on another
database, see Other databases. If your app doesn’t use pg yet, add it and its
type declarations:tsx and asks its questions with @inquirer/prompts (which hides the
key and secret as you type them). Add whichever your app doesn’t have yet as dev dependencies:You have an API key from the site
In the site’s CMS, API Keys → New API Key shows three things, once. Keep the page open until you’ve
stored them:
- the API key,
fsk_acme_… - its signing secret,
fss_…(none for a token-only key) - its signing mode: the key’s Request signing setting, on its Security tab. You’ll pick from Token + signature on writes, Token + signature on every request or Token only
Set it up
Create the table
One row per site; most apps have one.Run it with your migration tool, or once by hand:
migrations/create_attract_api_keys.sql
Terminal
Connect to the database
If your app already has a
pg Pool, export it from src/db.ts. Otherwise:src/db.ts
Create an encryption key
Generate 32 random bytes:Save the output as Then the helper that uses it:
Terminal
APP_ENCRYPTION_KEY wherever your app keeps its other secrets (a .env file
that isn’t committed, or your host’s secret settings), never in the database:.env
src/crypto.ts
Store and read the key
storeKey() builds a client first, so a bad URL, key or secret throws before anything is written.
The slug comes from the key itself (the acme in fsk_acme_…), and storing a key for a slug that
already has a row replaces that row. siteFromDatabase() builds the client from the row, once.src/attract.ts
Add the store-key script
It asks for what the CMS showed when the key was made, stores it, then makes one call to the site to
prove the key works:Add it to
scripts/store-key.ts
package.json. --env-file loads .env; leave it out where your host already sets the
variables:package.json
Store the key
Terminal
Terminal
Call the site
Anywhere in your app:
src/posts.ts
Replace the key
When you have a new key for the site from its CMS, or its signing mode changes, run the script again with the new values:Terminal
Forget the key
To take the key out of your app, delete its row:Terminal
Other databases
Only the connection and the two queries change. With Prisma, for example, model the same table:prisma/schema.prisma
storeKey() writes with prisma.attractApiKey.upsert({ where: { slug }, create, update }) and
siteFromDatabase() reads with prisma.attractApiKey.findMany(). Keep the encrypt() and decrypt()
calls exactly where they are.
Test it
The encryption round trip needs no database:tests/crypto.test.ts
fakeSite() from @fetchski/attract-cms-sdk/testing
(Testing).