atoms-api
atoms-api Skill
Generate api-config.json files for backend-only atoms-kit applications.
Overview
This is a backend-only variant of atoms-kit that defines just the data layer:
- Atoms: Your data entities (e.g., contacts, deals, tickets)
- Fields: Field types and validation
- Relations: Relationships between atoms
No frontend (pages, navigation, theme) is defined — this is purely for generating a REST API layer.
When to Use
- Backend-only APIs with no frontend
- Headless CMS / API-first architecture
- Generating API routes that will be consumed by external frontends
- Microservices that need CRUD operations
Minimal Example
A simple task API:
{
"version": "1.0",
"app": {
"name": "Task API",
"description": "Simple task management API"
},
"atoms": {
"task": {
"label": "Task",
"icon": "RiCheckboxLine",
"fields": {
"title": { "type": "text", "required": true },
"description": { "type": "longtext" },
"status": {
"type": "enum",
"values": ["todo", "in_progress", "done"],
"default": "todo"
},
"priority": {
"type": "enum",
"values": ["low", "medium", "high"],
"default": "medium"
},
"assigned_to": { "type": "text", "description": "Clerk user ID" },
"due_at": { "type": "datetime" },
"team_id": { "type": "text" }
},
"relations": {
"comments": { "type": "has_many", "atom": "comment", "foreignField": "root_id" }
}
}
}
}
This generates:
Tasktype/struct with all fields plusidandorg_id- CRUD endpoints at
/api/atoms/task - Auto-filtering by org_id, optional filtering by team_id
Schema Reference
Top-Level Structure
{
$schema?: string // Optional JSON schema reference
version: "1.0" // Required, must be "1.0"
app: AppMeta // App name and description
atoms: Record<string, AtomConfig> // Your data entities
}
AppMeta
{
name: string // App name
description?: string // Short description
}
AtomConfig
An atom is a data entity (like a database table).
{
label?: string // Display name (e.g., "Contact")
description?: string // Short description
icon?: IconName // Icon from the icon list (optional)
fields: Record<string, FieldConfig> // Field definitions
relations?: Record<string, RelationConfig> // Optional relations
}
FieldConfig
Shorthand form (type only):
"name": "text"
Full object form:
"name": {
"type": "text",
"label": "Name",
"required": true,
"description": "User's full name",
"default": "Untitled"
}
Field types:
| Type | Use Case | Example |
|---|---|---|
text |
Short text (names, titles) | { "type": "text" } |
longtext |
Multi-line content | { "type": "longtext" } |
email |
Email addresses | { "type": "email" } |
url |
Web URLs | { "type": "url" } |
number |
Numeric values | { "type": "number" } |
boolean |
True/false | { "type": "boolean", "default": false } |
datetime |
Date/time | { "type": "datetime" } |
json |
Arbitrary JSON | { "type": "json" } |
enum |
Fixed set of values | See below |
Enum fields:
"status": {
"type": "enum",
"label": "Status",
"values": ["open", "in_progress", "closed"],
"default": "open"
}
RelationConfig
{
type: "belongs_to" | "has_many" | "has_one"
atom: string // Target atom name
field?: string // Required for belongs_to
foreignField?: string // Required for has_many/has_one
}
Examples:
"relations": {
"company": {
"type": "belongs_to",
"atom": "company",
"field": "company_id"
},
"deals": {
"type": "has_many",
"atom": "deal",
"foreignField": "contact_id"
}
}
System Atoms
These are automatically available — do not define them in atoms:
| Atom | Purpose |
|---|---|
team |
Multi-tenancy teams |
comment |
Comments on any atom |
org_settings |
Organization configuration |
atom_definition |
Dynamic atom schemas |
Implicit Fields
Every atom automatically includes these fields — do NOT define them manually:
| Field | Type | Description |
|---|---|---|
id |
string |
Auto-generated unique identifier (UUID) |
org_id |
string |
Organization ID for multi-tenancy (auto-populated from auth) |
These are added by the API layer on all operations. Your fields definition should only include domain-specific fields.
Multi-Tenancy Model
Atoms-kit supports two levels of data scoping:
org_id — Organization Isolation (Hard Boundary)
- Source: Extracted from authentication token (Clerk JWT or API key default)
- Behavior: Auto-filtered on all GET queries, auto-injected on all writes
- Purpose: Complete data isolation between organizations/tenants
- User control: None — it's enforced by the API
User authenticates → Token contains org_id → API filters all data by org_id
team_id — Team Grouping Within Org (Soft Boundary)
- Source: User-defined field in your atom schema
- Behavior: Manual filtering via
?filter[team_id]=xxx - Purpose: Sub-grouping data within an organization (e.g., sales team vs engineering team)
- User control: Optional — add
team_idfield to atoms that need it
// In your atom definition:
"fields": {
"name": { "type": "text" },
"team_id": { "type": "text", "description": "Team this belongs to" }
}
Key difference:
org_id= Hard isolation between companies/accounts (automatic)team_id= Soft grouping within an org (opt-in, manual)
Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer <token>
Backend (Server-to-Server)
Set the SCOUTOS_API_KEY environment variable. The API will:
- Accept requests without Clerk JWT
- Use
"default"as the org_id - Allow full access to that org's data
SCOUTOS_API_KEY=sk_live_xxx
Frontend (User Sessions)
When using Clerk authentication:
- Clerk JWT is passed in Authorization header
userIdandorgIdare extracted from the token- All queries are automatically scoped to that org
Auth Response Codes
| Status | Meaning |
|---|---|
401 |
Missing or invalid token |
400 |
Valid token but missing org membership |
Response Formats
All API responses use a consistent JSON envelope.
Single Record
// GET /api/atoms/task/abc123
{
"data": {
"id": "abc123",
"title": "Build API",
"status": "in_progress",
"priority": "high",
"assigned_to": "user_456",
"team_id": "team_789",
"org_id": "org_001"
}
}
List Records
// GET /api/atoms/task?limit=10
{
"data": [
{ "id": "abc123", "title": "Build API", ... },
{ "id": "def456", "title": "Write tests", ... }
]
}
Note: The list response returns a flat array under data. Pagination metadata may be added via query params but is not included in the response body.
Create/Update Record
// POST /api/atoms/task
// Request body: { "title": "New task", "status": "todo" }
{
"data": {
"id": "xyz789",
"title": "New task",
"status": "todo",
"org_id": "org_001"
}
}
Delete Record
// DELETE /api/atoms/task/abc123
{
"success": true
}
Error Response
{
"error": "Unauthorized"
}
{
"error": "Unknown atom: invalid_atom_name"
}
Type Mappings
When implementing the API in TypeScript or Rust, use these type mappings:
TypeScript
| Field Type | TypeScript Type |
|---|---|
text |
string |
longtext |
string |
email |
string |
url |
string |
number |
number |
boolean |
boolean |
datetime |
string (ISO 8601) |
json |
unknown |
enum |
string (union type recommended) |
Rust
| Field Type | Rust Type |
|---|---|
text |
String |
longtext |
String |
email |
String |
url |
String |
number |
i64 |
boolean |
bool |
datetime |
chrono::DateTime<chrono::Utc> |
json |
serde_json::Value |
enum |
enum (derive Serialize, Deserialize) |
PostgreSQL
| Field Type | SQL Type |
|---|---|
text |
VARCHAR(255) |
longtext |
TEXT |
email |
VARCHAR(255) |
url |
VARCHAR(2048) |
number |
BIGINT |
boolean |
BOOLEAN |
datetime |
TIMESTAMPTZ |
json |
JSONB |
enum |
VARCHAR(64) + CHECK constraint |
Recommended Indexes
CREATE INDEX idx_{atom}_org_id ON {atom}(org_id);
CREATE INDEX idx_{atom}_team_id ON {atom}(team_id); -- if team_id exists
CREATE INDEX idx_{atom}_created_at ON {atom}(created_at DESC);
-- Add indexes on frequently filtered enum fields (status, priority, etc.)
Implementation Notes
PUT Replaces Entire Document
The PUT /api/atoms/{atom}/{id} endpoint performs a full replacement, not a partial merge. Include all fields in your request body.
// PUT /api/atoms/task/abc123
// This will remove "assigned_to" if not included:
{ "title": "Updated title", "status": "done" }
// Correct approach — include all fields:
{ "title": "Updated title", "status": "done", "assigned_to": "user_456", ... }
DELETE is Idempotent
DELETE returns { "success": true } even if the record didn't exist. This allows safe retries.
org_id is Auto-Injected
On POST and PUT:
- Do NOT include
org_idin request body - It will be ignored if included
- The API injects the org_id from the auth token
Default Values
When creating a record:
- Explicit values in request body take precedence
- Field-level
defaultvalues from schema are applied null/missing fields stay null
OpenAPI Example
Here's a minimal OpenAPI 3.0 specification for one atom:
openapi: 3.0.0
info:
title: Task API
version: 1.0.0
paths:
/api/atoms/task:
get:
summary: List tasks
parameters:
- name: filter[status]
in: query
schema:
type: string
enum: [todo, in_progress, done]
- name: filter[team_id]
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
default: 50
responses:
'200':
description: List of tasks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Task'
post:
summary: Create a task
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TaskCreate'
responses:
'200':
description: Created task
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Task'
/api/atoms/task/{id}:
get:
summary: Get a task
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Task details
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Task'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Task:
type: object
properties:
id:
type: string
title:
type: string
status:
type: string
enum: [todo, in_progress, done]
priority:
type: string
enum: [low, medium, high]
assigned_to:
type: string
nullable: true
team_id:
type: string
nullable: true
org_id:
type: string
TaskCreate:
type: object
required: [title]
properties:
title:
type: string
status:
type: string
enum: [todo, in_progress, done]
default: todo
priority:
type: string
enum: [low, medium, high]
default: medium
assigned_to:
type: string
team_id:
type: string
Error:
type: object
properties:
error:
type: string
securitySchemes:
BearerAuth:
type: http
scheme: bearer
security:
- BearerAuth: []
Available Icons
Choose from these Remix Icon names (optional, for tooling/display purposes):
RiDashboardLine, RiHome4Line, RiSettingsLine, RiSearchLine,
RiNotification3Line, RiInboxLine, RiAddLine, RiFilter2Line,
RiUserLine, RiGroupLine, RiTeamLine, RiUserHeartLine, RiUserSearchLine,
RiContactsBookLine, RiCustomerService2Line,
RiMoneyDollarCircleLine, RiShoppingCartLine, RiPriceTag3Line, RiCouponLine,
RiCheckboxLine, RiTodoLine, RiListCheck2, RiFolderLine, RiBriefcaseLine,
RiFileListLine, RiBookLine,
RiCalendarLine, RiVideoLine, RiTimeLine,
RiMegaphoneLine, RiMailLine, RiChat1Line,
RiBarChartLine, RiPieChartLine,
RiTicketLine,
RiCodeSSlashLine, RiLightbulbLine, RiAtomLine, RiAppsLine, RiDatabase2Line,
RiGlobalLine, RiHashtag, RiLink, RiLockLine, RiStarLine
Patterns & Conventions
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Atom names | singular, snake_case | contact, kb_article |
| Field names | snake_case | first_name, close_date |
Common Field Patterns
User references (Clerk user IDs):
"owner_id": { "type": "text", "label": "Owner", "description": "Clerk user ID" },
"assigned_to": { "type": "text", "label": "Assigned To", "description": "Clerk user ID" },
"created_by": { "type": "text", "label": "Created By", "description": "Clerk user ID" }
Foreign keys (belongs_to relations):
"company_id": { "type": "text", "label": "Company" },
"project_id": { "type": "text", "label": "Project" }
Standard timestamp fields:
"created_at": { "type": "datetime" },
"updated_at": { "type": "datetime" },
"due_at": { "type": "datetime", "label": "Due Date" },
"resolved_at": { "type": "datetime", "label": "Resolved At" }
Relation Patterns
Comments on any atom:
"comments": {
"type": "has_many",
"atom": "comment",
"foreignField": "root_id"
}
Belongs_to (child → parent):
"company": {
"type": "belongs_to",
"atom": "company",
"field": "company_id"
}
Has_many (parent → children):
"contacts": {
"type": "has_many",
"atom": "contact",
"foreignField": "company_id"
}
Agent Workflow
When asked to create a backend API from api-config.json:
-
Parse the config. Load the JSON and validate:
- All atoms have valid field types
- Relations reference atoms that exist
- Required fields are marked
-
Generate types. Create TypeScript interfaces or Rust structs:
- One type per atom
- Include implicit fields (
id,org_id) - Handle enums as union types
-
Generate routes. For each atom, create CRUD endpoints:
GET /api/atoms/{atom}— List with filteringPOST /api/atoms/{atom}— CreateGET /api/atoms/{atom}/{id}— Get onePUT /api/atoms/{atom}/{id}— ReplaceDELETE /api/atoms/{atom}/{id}— Delete
-
Implement auth middleware. Extract org_id from:
- Bearer token (JWT with org_id claim)
- Or API key env var (default to "default" org)
-
Add multi-tenancy. Every query must filter by org_id, every write must inject it.
Common Validation Errors
| Error | Cause | Fix |
|---|---|---|
atom "X" is not defined |
Relation references non-existent atom | Add the atom to atoms |
belongs_to relation must specify "field" |
Missing foreign key field | Add "field": "atom_id" |
has_many relation must specify "foreignField" |
Missing inverse field | Add "foreignField": "parent_id" |
duplicate atom name "X" |
Two atoms with same name | Use unique atom names |
invalid field type "X" |
Unknown field type | Use: text, longtext, number, boolean, datetime, json, enum, email, url |
Converting from app-config.json
If you have an existing app-config.json and want to extract just the backend:
- Remove
pages,navigation, andthemesections - Remove any frontend-specific field options (like
card,list_columns, etc. from pages) - Keep only
version,app, andatoms
The resulting api-config.json will define the same data model for API-only use.
CLI Usage
# Validate an api-config.json
atoms validate api-config.json
# Initialize a backend-only project
atoms init ./my-api --config api-config.json