@athenox/talos

Talos: a backend you describe instead of write. Declare a schema, write the rules in plain language, map URLs to sentences; a small built-in model turns them into query plans.
594
Install
npm install @athenox/talos
Latest Version:0.1.2
License:MIT
Last Updated:Sep 27, 2026
Links: npm
Maintainer: athenox

Talos

A backend you describe instead of write. Declare your tables, write the rules in plain language, map each URL to a sentence — a small language model, built for this and running on your CPU, turns each sentence into a query plan, and the runtime runs it safely against your database.

import { defineApp, defineRoutes } from '@athenox/talos'

const app = defineApp({
  prompt: `
    show the recipes.
    a member may add a recipe, and change or delete their own.`,
  schema: { tables: [{
    as: 'recipes', table: 'recipes',
    columns: { id: 'id', title: 'title', course: 'course', minutes: 'minutes', author: 'author_id' },
    enums: { course: ['main', 'starter', 'dessert'] },
    roles: { write: ['member', 'admin'] },
  }] },
})

// member (a guard) and asMember (the caller's role): see Accounts below
export default defineRoutes(app, {
  'GET    /api/recipes':     { say: 'show the recipes', orderBy: '-id', limit: 20 },
  'GET    /api/recipes/:id': { say: 'show the recipe {id}', one: true },
  'POST   /api/recipes':     { say: 'add a recipe', guard: member, ctx: asMember },
  'DELETE /api/recipes/:id': { say: 'delete the recipe {id}', guard: member, ctx: asMember, where: (_, me) => ({ author_id: me.id }) },
})

No SQL, no controllers, no migrations — and no database to set up if you do not want one: without a store, Talos keeps its own SQLite file built from the schema.

  • The model plans, the runtime decides. The model reads the schema, the rules and the request, and generates the plan (operation, table, filters, values) by pointing into what it read. Permissions, ownership, validation and the SQL are ordinary deterministic code.
  • Compiled once. A route's sentence goes through the model on its first call; the plan is kept with its parameters as slots and replayed without the model afterwards. A route then costs what its SQL costs.
  • A full backend kit around it: accounts and sessions, uploads with image processing, full-text search, recommendations, feeds and sitemaps, CSV import, rate limiting, schema sync for SQLite and MySQL/MariaDB, Drizzle schemas.
  • Free-text requests too. app.run('the easy desserts under 30 minutes', ctx) answers a sentence nobody wrote a route for.

Contents — Install · Guide: the app · the schema · routes · free text · accounts · files · search · databases · telemetry · Results · Environment · Full documentation

Install

npm install @athenox/talos

Node 22.13 or later. The model (≈ 25 MB with its text embedder, ONNX, int8, CPU) ships in the package. It comes with h3 1.x for its HTTP helpers (the version Nuxt 4 and Nitro use, so they share it); sharp is needed only to process images. With Nuxt, keep Talos out of the bundle — it loads its model from its own folder:

// nuxt.config.ts
nitro: { externals: { external: ['@athenox/talos', 'onnxruntime-node', 'sharp'] } }

Guide

1. The app

export const app = defineApp({
  prompt,          // the rules, in prose — read by the model with every request
  schema,          // { tables, functions? }
  store?,          // your database; left out: Talos's own (.talos/data.db)
  seed?,           // rows for Talos's own database, put into tables still empty
  actions?,        // implementations of the schema's functions
  telemetry?,      // what this instance reports (on by default), or false
})

The prompt is written the way you would explain the app to a colleague: what can be shown, who may do what.

show the recipes, the cooks and the comments.
a member may add a recipe, and change or delete their own.
an admin may do anything.

Prompts, route sentences and requests can be in English or French — the model reads both, and a French request reaches an English schema through says (says: ['recettes'] on recipes).

The prompt guides the plan; it grants nothing. What a caller may do is the schema's roles and your route guards — a model that is right 99 % of the time is not a permission check.

2. The schema

{
  as: 'recipes', table: 'recipes',                    // the model's name, the physical table
  says: ['dishes', 'recettes'],                       // how people call it (any language)
  columns: { id: 'id', title: 'title', course: 'course', minutes: 'minutes', author: 'author_id', date: 'created_at' },
  columnSays: { minutes: ['time', 'duration'] },
  enums: { course: ['main', 'starter', 'dessert'] },  // declared values
  types: { minutes: 'number' },
  relations: { author: { table: 'users', on: 'author_id', columns: { id: 'id', name: 'name' } } },
  lists: { comments: { table: 'comments', on: 'recipe_id', columns: { id: 'id', body: 'body' }, orderBy: 'created_at' } },
  tallies: { likes: { table: 'likes', on: 'recipe_id' } },
  searchable: ['title'],
  rules: { title: { required: 'insert', trim: true, min: 3, max: 120, message: 'A title of 3 to 120 characters' } },
  defaults: { insert: { created_at: () => new Date().toISOString() } },
  children: [{ table: 'likes', on: 'recipe_id', cascade: true }],
  hidden: [],
  roles: { read: ['guest', 'member', 'admin'], write: ['member', 'admin'], columns: { featured: ['admin'] } },
}

The keys of columns are logical names — what the model reads; the values are physical — what the database has and what rows come back with. Keep them equal and in English when you can (the model knows English schema words best) and put the words people use in says / columnSays.

group fields
read by the model as, columns, enums, flags (booleans as two words), says, columnSays
reading relations (an object per row), lists (an array per row), many (through a join table), tallies (counts), computed, hidden, searchable, view, types, json, read
writing rules, defaults, hash, slugs, files, infer (filled by meaning), writable, children (cascade / restrict), after (functions to run), write
permissions roles (read, write, create, remove, columns), ownerColumn + ownerFrom (rows pinned to the caller)

Rules: required (true or 'insert'), min, max, email, pattern, trim, unique (or its message), uniqueWith, message. A failed rule is a 400 with data.errors: [{ msg, column }], the shape forms expect.

Functions are side effects the prose may name:

schema: { tables, functions: [{ name: 'sendNewsletter', params: ['subject'], required: ['subject'], roles: ['admin'], says: ['send the newsletter'] }] },
actions: { sendNewsletter: async ({ args, ctx }) => ({ sent: 120 }) },

→ every field: docs/schema.md

3. Routes

A sentence

const del = app.route('delete the recipe {id}', { ops: ['DELETE'] })
const { status, result, plan, callId } = await del({ id: 3 }, { userId: 7, role: 'member' })

{k} is a parameter, [... {k} ...] is dropped when k is missing, {k:no|yes} says a boolean with one of two words. The first call asks the model; later ones replay the plan with new values. Before the model reads a sentence, a misspelt word of the app or a misspelt verb is corrected, and an opening verb (delete, add, change, show, and their French forms) fixes the operation.

A whole API as a table

export default defineRoutes<Member>(app, {
  'GET /api/recipes': {
    say: 'show the recipes', orderBy: '-date', limit: 12, total: true,
    fields: ['id', 'title', 'course', 'likes', 'author.name'],
    where: p => (p.course ? { course: p.course } : {}),
    shape: p => ({ search: p.q, page: Number(p.page) || 1 }),
  },
  'GET /api/recipes/:id':    { say: 'show the recipe {id}', one: true, notFound: 'No such recipe' },
  'POST /api/recipes':       { say: 'add a recipe', guard: member, ctx: asMember, stamp: (_, me) => ({ author_id: me.id }) },
  'PUT /api/recipes/:id':    { say: 'change the recipe {id}', guard: member, ctx: asMember, where: own },
  'DELETE /api/recipes/:id': { say: 'delete the recipe {id}', guard: member, ctx: asMember, where: own },
  'POST /api/auth/login':    users.login,              // any h3 handler sits in the table as it is
}, { guards: { member }, defaults: { firstError: true } })
  • The HTTP method fixes the operation (GET → read, POST → create or call, PUT/PATCH → update, DELETE → delete).
  • URL segments (/:id, /:slug) name the row; they replace whatever the model wrote on those columns. The body's fields are written by name.
  • guard runs first and its user is given to ctx, where, stamp, shape and reply. ctx is what roles are checked against.
option
say the sentence
guard (event, params) => user; throws to refuse
ctx user => ({ userId, role })
where conditions set by code: { col: v } or (p, user) => ({...}); { gte }, { lt }, { between }, { ne }, { in }, { null }, any: [...] (or), 'relation.column'
stamp columns the route writes itself: (p, user) => ({ author_id: user.id })
fill, only, form defaults for missing parameters, allowed body fields, HTML-form checkboxes and empty fields
shape per-request options: (p, user, event) => ({ search, page, ... })
fields, row, reply what the answer holds
notFound, firstError, pin, download 404 message, one error only, which segments name the row, send a file
orderBy, limit, page, total, one '-date', ['-likes', 'title'], 'minutes~' (empty last); total adds { total, page, limit }
search, find, similar, recommend ranking — section 7
lists, counts, sums, returning which lists to load, extra counts and sums, return written rows
rows, upsert, ignore, dedupe, before bulk inserts, insert-or-update, skip duplicates, remove duplicates, a hook before the write

Answers. 200 with the row (one), the rows, { rows, total, page, limit }, { id } for a creation, { success: true } for an update or delete — or what reply returns. 400 for failed rules, 401/403 from guards and roles, 404 when nothing matched, 422 when the request is not something this app does. Every answer carries x-talos-plan (the sentence and its plan — read it once when you add a route) and x-talos-call (the model call).

→ docs/routes.md

4. Free-text requests

const { status, result, plan, callId } = await app.run('the easy desserts under 30 minutes', ctx, { ops: ['SELECT'], limit: 10 })

run asks the model every time: a search box, an assistant, an admin console. The same roles and ownership apply. decide(sentence, ctx) gives the plan without running it; execute(plan, ...) runs a plan however it was made.

5. Accounts

import { accounts, hashPassword, verifyPassword } from '@athenox/talos'

export const users = accounts<Member>({
  store: app.store, table: 'users', expose: ['id', 'name', 'role'], signUp: ['name'], minPassword: 8,
  hash: hashPassword, verify: verifyPassword,
  session: { set: (e, user) => setUserSession(e, { user }), get: async e => (await getUserSession(e)).user, clear: clearUserSession },
})
export const member = users.guard()                                              // the account, or 401
export const asMember = (me: Member) => ({ userId: me.id, role: me.role })
export const own = (_: unknown, me: Member) => (me.role === 'admin' ? {} : { author_id: me.id })

Handlers: users.register, users.login (JSON or a plain HTML form), users.logout, users.me, users.change, and users.forgot / users.reset with reset: { table, owner, send }. users.guard(check?) re-reads the account (a disabled one is refused). Any session store works; above, nuxt-auth-utils.

→ docs/accounts.md

6. Files and images

files: {
  photo: {
    dir: 'uploads/recipes', url: '/uploads/recipes/', accept: /^image\//, maxBytes: 12_000_000,
    image: { width: 1600, height: 1600, format: 'webp', quality: 82 },
    variants: { thumb: { width: 640, height: 480, fit: 'cover', format: 'webp' } },
  },
}

A multipart request carries the file under the column's name. It is checked, resized, re-encoded, stripped of its metadata (a phone photo carries its GPS position), stored under a random name, and the column set to its URL; variants go to their own columns. Replacing it or deleting the row removes the old file. Serve them with serveFiles('uploads/recipes').

→ docs/files.md

7. Search, similar rows, recommendations

They rank the rows the plan chose — conditions and permissions still apply — with words and with meaning (Talos's own French/English text embedder).

shape: p => ({ search: p.q })                                            // over `searchable`
shape: p => ({ find: { q: p.q, fields: { title: 3, body: 1 }, html: true } })   // weighted, prefixes, typos, meaning
shape: p => ({ similar: { to: { id: Number(p.id) }, text: ['title', 'body'] } })
shape: (_, me) => ({ recommend: { user: me.id, interactions: { table: 'likes', user: 'user_id', item: 'recipe_id' }, text: ['title'], date: 'created_at' } })

recommend scores each row by closeness to what the reader liked, what readers with the same likes liked, popularity and recency, and leaves out what they already saw.

→ docs/search.md

8. Databases

none Talos's own SQLite file (.talos/data.db, TALOS_DB), tables and columns made from the schema at start, never dropped; seed fills empty tables; await app.ready
SQLite store: sqliteStore(new DatabaseSync('app.db')) (Node's or better-sqlite3)
MySQL / MariaDB store: mysqlStore(mysql.createPool(url))
Drizzle schema: { tables: fromDrizzle(drizzleSchema) }, syncSchema(store, drizzleSchema) to create what is missing

Also: parseCsv, toCsv, importCsv (header aliases, deduplication, a report), connector (an external JSON API into your fields), feed('rss' | 'sitemap' | 'sitemap-news'), rateLimit.

9. Telemetry, ids and feedback

app.instanceId                 // this instance — always Talos's own
result.callId                  // every model call (also plan.callId and the x-talos-call header)
await app.feedback(callId, { rating: 4, text: 'right rows, wrong order' })   // a text, a rating 1–5, or both

Each instance reports to stats.talos.korollr.com, so that someone who comes with an instance id or a call id can be helped:

part
errors exceptions in the 5 s after an answer, tied to the call (a crash is sent by the next start)
operations each plan and answer — operation, table, columns, status, counts; values only when declared
prompts the app's prompt, route templates, free-text requests anonymised (unknown words → <w>, numbers → #)
performance latency per call; every minute, Talos's and the process's load
config Talos and model versions, Node, platform
schema tables and columns — never a row

Every option, with its default — set one to false to stop sending it:

telemetry: {
  errors: true,          // exceptions right after an answer, tied to the call
  operations: true,      // plans and answers: operation, table, columns, status (values masked)
  prompts: true,         // the app's prompt, route templates, anonymised requests
  performance: true,     // latency per call, load every minute
  config: true,          // Talos/model versions, Node, platform — once
  schema: true,          // tables and columns — once, never a row
  errorWindowMs: 5000,   // how long after an answer an error still counts
  loadEveryMs: 60000,    // load sampling period
}
telemetry: { prompts: false }   // everything but what users typed and the prompt
telemetry: false                // nothing

What each one sends exactly, with an example event and what you lose without it: docs/telemetry.md.

TALOS_TELEMETRY=0 or DO_NOT_TRACK=1 turn it off for the whole process. An app chooses what is sent, not where.

→ docs/telemetry.md

Results

Measured on 164 sentences recorded from three real applications ported to Talos (a newspaper, an association site, a plugin store):

right plans
routes, as used in production 99.4 %
the model alone, no hints 98.2 %
a typo in the sentence 98.1 %
another verb (remove for delete) 94.7 %

A route compiles in about 200 ms on a CPU, once. Ported, those applications' backends went from 13,152 lines to 6,518, from 1,807 to 661, and from 1,748 to 918. A complete application built with it — a recipe-sharing site (Nuxt 4, Tailwind 4: accounts, photos, comments, favourites, recommendations, search by sentence) — has about 270 lines of backend.

Environment

variable
TALOS_DB Talos's own database file (default .talos/data.db)
TALOS_THREADS cap on the model's CPU threads
TALOS_MODEL_DIR load the weights from elsewhere
TALOS_TELEMETRY=0 no telemetry for the whole process

Full documentation

The pages below ship in the package (node_modules/@athenox/talos/docs/):

Getting started · Schema · Routes · Accounts · Files · Search · Telemetry · Reference

License

MIT © Athenox Development

Related Packages

@truxie/nitro

Nitro adapter for truxie — controller dispatcher and handler utilities

1,077
h3-express

Express polyfill for h3

5,877 7
@ifiander/gachacenter

GachaCENTER. 创作者档案网站完整源码:Nuxt SSR、MySQL、审核与管理后台

297
@jfungus/ratelimit-h3

H3/Nitro middleware for @jfungus/ratelimit

505 9
nuxt-feathers-zod

Nuxt 4 module for FeathersJS v5 with embedded or remote mode, Zod-first services...

11,111 1

Version History

Version Released License
0.1.2 MIT
0.1.1 Proprietary
0.1.0 Proprietary