atoms-api

SKILL.md

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:

  • Task type/struct with all fields plus id and org_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_id field 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
  • userId and orgId are 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_id in request body
  • It will be ignored if included
  • The API injects the org_id from the auth token

Default Values

When creating a record:

  1. Explicit values in request body take precedence
  2. Field-level default values from schema are applied
  3. 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:

  1. Parse the config. Load the JSON and validate:

    • All atoms have valid field types
    • Relations reference atoms that exist
    • Required fields are marked
  2. Generate types. Create TypeScript interfaces or Rust structs:

    • One type per atom
    • Include implicit fields (id, org_id)
    • Handle enums as union types
  3. Generate routes. For each atom, create CRUD endpoints:

    • GET /api/atoms/{atom} — List with filtering
    • POST /api/atoms/{atom} — Create
    • GET /api/atoms/{atom}/{id} — Get one
    • PUT /api/atoms/{atom}/{id} — Replace
    • DELETE /api/atoms/{atom}/{id} — Delete
  4. Implement auth middleware. Extract org_id from:

    • Bearer token (JWT with org_id claim)
    • Or API key env var (default to "default" org)
  5. 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:

  1. Remove pages, navigation, and theme sections
  2. Remove any frontend-specific field options (like card, list_columns, etc. from pages)
  3. Keep only version, app, and atoms

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
Weekly Installs
2
Repository
twilson63/atoms
First Seen
Feb 12, 2026
Installed on
amp2
gemini-cli2
github-copilot2
codex2
kimi-cli2
opencode2