Authentication
Public reads need no auth. You only need a key to read drafts and private content, or to write (create, update, delete posts) and manage webhooks.
Keys are flg_ tokens, hashed at rest, scoped, revocable, and not expiring. Up to 5 per account.
Creating a key
- Open your Floggy dashboard.
- Go to Settings -> Developer.
- Create a key, pick its scopes (or a preset), and copy it.
The full key (flg_ followed by 32 characters) is shown once at creation. Floggy stores only a hash, so it cannot show you the key again. If you lose it, revoke and create a new one.
Using a key
Keys are created in the Dashboard (Settings -> Developer) and used by the SDK and CLI automatically. With the SDK, pass it as key:
import { createClient } from "@floggy/cms";
const floggy = createClient({
project: "projecta",
key: process.env.FLOGGY_API_KEY, // only needed for drafts / private content
});
Scopes
A request succeeds when the key carries every scope the endpoint requires. The * scope satisfies all of them. A scoped key that is missing a required scope returns 403 {"error":"Insufficient scope","required":[...]}.
| Scope | Grants |
|---|---|
posts:read |
Read your own posts, including drafts and private posts. |
posts:write |
Create, update, publish, and unpublish posts, plus every bulk action (bulk delete included). |
posts:delete |
Delete a single post. |
uploads:write |
Upload media. |
notes:read |
Read notes. |
notes:write |
Create and update notes. |
tasks:read |
Read tasks. |
tasks:write |
Create and update tasks. |
profile:read |
Read profile / account settings. |
profile:write |
Update profile / account settings. |
analytics:read |
Read analytics. |
newsletter:read |
Read newsletter subscribers and data. |
newsletter:write |
Manage newsletter subscribers and sends. |
webhooks:read |
List webhook endpoints. |
webhooks:write |
Create, update, delete webhook endpoints. |
collections:read |
Read collections, entries, versions, labels. |
collections:write |
Create and update collections, entries, versions, labels. |
collections:delete |
Delete collections and entries. |
* |
Full access to everything above. Trusted automation only. |
The three post scopes
Writing posts uses three scopes, and they do not nest:
| Scope | Covers |
|---|---|
posts:read |
posts.mine, posts.getById, posts.draft. Drafts and private posts included. |
posts:write |
posts.create, posts.update, posts.publish, posts.unpublish, and all of posts.bulk. |
posts:delete |
posts.delete only. |
posts:write does not imply posts:read, so an agent that creates a draft and then previews it needs both.
Note the asymmetry on delete: posts.delete(id) needs posts:delete, but posts.bulk("delete", ids) runs through the bulk endpoint and needs only posts:write. A key you think of as unable to delete can still bulk-delete.
A key acts as its owner
Authentication decides whose content you touch. createClient({ project }) selects the blog for public reads only. Every authenticated call - posts.mine, posts.getById, posts.draft, every write, all of collections - answers as the key's owner. Build a client with someone else's project and your own key and you will read their blog while writing to yours. Keep the two on the same account unless you mean otherwise.
When a scope is missing
A key missing a required scope gets 403 {"error":"Insufficient scope","required":["posts:write"]}. required is the endpoint's full requirement list, not the difference against what your key already has.
The SDK turns that response into a ScopeError: a FloggyError subclass with status 403, code insufficient_scope, and a required array. Use it to name the missing permission instead of reporting a bare "forbidden":
import { ScopeError } from "@floggy/cms";
try {
await floggy.posts.publish(id);
} catch (err) {
if (err instanceof ScopeError) {
throw new Error(
`This Floggy key needs ${err.required.join(", ")}. Regenerate it in Settings > Developer with that scope.`,
);
}
throw err;
}
A 403 whose body does not name scopes is not a scope problem and stays a plain FloggyError. A missing, invalid, or revoked key is a 401, not a 403.
Presets
The create-key UI offers three presets so you do not have to hand-pick scopes:
| Preset | Scopes | Use it for |
|---|---|---|
| Read-only | all *:read (posts:read, notes:read, tasks:read, profile:read, analytics:read, newsletter:read, webhooks:read, collections:read) |
Anything a frontend touches. Safe to expose. |
| Editor | Read-only plus posts:write, posts:delete, uploads:write, collections:write, collections:delete |
CMS authoring tools, CI that publishes posts. |
| Full access | * |
Your own trusted CLI and agents only. Never in a frontend. |
For a blog frontend that only consumes published content you usually need no key at all. Add a Read-only key only if the frontend must also fetch drafts or private posts (for example a preview build).
Key safety and disclosure
The keys that leak in practice are the ones embedded in frontends. Minimize the blast radius before that happens.
- Never commit keys. Keep them out of git, source, and client bundles. Use a
.gitignored.env. - Use environment variables. The convention across Floggy tooling is
FLOGGY_API_KEY.export FLOGGY_API_KEY="flg_xxxxxxxx..." - Use a Read-only key for anything a browser can see. If a Read-only key leaks, the holder can read content that is already public anyway, plus your drafts and private posts. They cannot write, delete, or change settings.
- Never ship a Full-access (
*) or Editor key to a frontend. A leaked Editor key can publish and delete your posts. A leaked*key is a full account compromise. - Prefer server-side fetching. Call the API from your server (server components, route handlers, build steps) so the key never reaches the client. A purely public blog frontend can skip the key entirely.
- Rotate by revoke + recreate. There is no expiry and no in-place rotation. To rotate: create the new key, deploy it, then revoke the old one in Settings -> Developer.
- Audit usage. Each key records a
lastUsedAt. If a key shows unexpected activity, revoke it immediately.
What a leaked Read-only key can and cannot do
| Can | Cannot |
|---|---|
| Read your published posts (already public) | Create, edit, or delete posts |
| Read your drafts and private posts | Upload media |
| Read analytics, notes, tasks, profile, newsletter data (read scopes) | Change any account settings |
| List webhook endpoints | Manage webhooks, manage subscribers, or anything *:write |
This is why Read-only is the right default for exposed surfaces: the worst case is read access to content you mostly publish anyway. Keep write and delete power in keys that never leave your servers.