Hands-on walkthroughs that build on the Quickstart. Each tutorial assumes you have Snaapi running and can log into the Admin Console.
Create a full blog backend with posts, authors, tags, roles, and API access.
Open the Admin Console and create three resources:
- authors -- fields:
name(string, required),email(string, required) - tags -- fields:
name(string, required) - posts -- fields:
title(string, required)body(text)status(enum, values:draft,published,archived)slug(string, generated fromtitle-- set the generate transform toslugify)author_id(relation, many-to-one to authors)tag_ids(relation, many-to-many to tags)
Create two roles in the Admin Console:
- editor -- permissions: read and update on
posts(all fields) - viewer -- permissions: read-only on
posts(fields:id,title,body,slug,status,created_at)
Admins already have full access by default, so no admin permissions are needed.
curl -X POST http://localhost:8000/token \
-H "Content-Type: application/json" \
-d '{"email":"your@email.com","password":"your-password"}'# Create an author
curl -X POST http://localhost:8000/v1/authors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sna_YOUR_TOKEN" \
-d '{"name": "Jane Doe", "email": "jane@example.com"}'
# Create a tag
curl -X POST http://localhost:8000/v1/tags \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sna_YOUR_TOKEN" \
-d '{"name": "tutorial"}'
# Create a post (use the author ID and tag ID from the responses above)
curl -X POST http://localhost:8000/v1/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sna_YOUR_TOKEN" \
-d '{
"title": "Getting Started with Snaapi",
"body": "Snaapi makes building APIs easy...",
"status": "published",
"author_id": "<author-id>",
"tag_ids": ["<tag-id>"]
}'
# List published posts
curl "http://localhost:8000/v1/posts?status=published" \
-H "Authorization: Bearer sna_YOUR_TOKEN"
# Update a post
curl -X PUT http://localhost:8000/v1/posts/<post-id> \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sna_YOUR_TOKEN" \
-d '{"status": "archived"}'
# Delete a post
curl -X DELETE http://localhost:8000/v1/posts/<post-id> \
-H "Authorization: Bearer sna_YOUR_TOKEN"See Relations for more on how Snaapi resolves linked resources, and Roles for details on the role system.
Let users manage their own data while keeping it isolated from other users.
Create a tasks resource with these fields:
title(string, required)description(text)status(enum, values:todo,in_progress,done)owner_id(relation, many-to-one to users)
In the Admin Console, create permissions for the user role on the tasks
resource:
| Action | Fields | Filter / Check |
|---|---|---|
| create | title, description, status | check: owner_id eq claim id |
| read | id, title, description, status, created_at | filter: owner_id eq claim id |
| update | title, description, status | check: owner_id eq claim id |
| delete | -- | check: owner_id eq claim id |
The claim "id" references the authenticated user's ID. On creates, Snaapi
auto-injects the user's ID into owner_id. On reads, the filter ensures users
only see their own tasks. On updates and deletes, the check blocks access to
records owned by someone else.
Create API keys for two different users (User A and User B). Create a task as User A, then try to read it as User B:
# As User B -- returns an empty list, not User A's tasks
curl http://localhost:8000/v1/tasks \
-H "Authorization: Bearer sna_USER_B_TOKEN"See Permissions for the full constraint reference including operators, claims, and auto-injection.
Subscribe to live changes using Server-Sent Events (SSE) and update your UI as data changes.
Use the browser's EventSource API to open a persistent connection:
const token = "sna_YOUR_TOKEN";
const source = new EventSource(
`http://localhost:8000/stream/tasks?events=created,updated,deleted`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const counter = document.getElementById("task-count");
let count = 0;
source.addEventListener("tasks.created", (e) => {
count++;
counter.textContent = count;
const task = JSON.parse(e.data).new;
console.log("New task:", task.title);
});
source.addEventListener("tasks.deleted", (e) => {
count--;
counter.textContent = count;
});
source.onerror = () => console.log("Reconnecting...");In another terminal, create or update tasks via the API. Your EventSource listener receives each change in real time -- no polling required.
See Realtime for the full SSE reference including event filtering, field selection, and permission-based filtering.

