# Getting started

## 1. Describe the app

One `defineApp` holds everything the backend knows: the schema, the rules in
prose, and optionally the database.

```ts
// server/utils/talos.ts
import { defineApp, hashPassword } from '@athenox/talos'

export const app = defineApp({
  prompt: `
    show the recipes, the cooks and the comments.
    a member may add a recipe, and change or delete their own.
    a member may comment.
    an admin may do anything.`,
  schema: {
    tables: [
      {
        as: 'recipes', table: 'recipes', says: ['dishes', 'recettes'],
        columns: { id: 'id', title: 'title', body: 'body', course: 'course', minutes: 'minutes', author: 'author_id', date: 'created_at' },
        enums: { course: ['main', 'starter', 'dessert'] },
        types: { minutes: 'number' },
        defaults: { insert: { created_at: () => new Date().toISOString() } },
        relations: { author: { table: 'users', on: 'author_id', columns: { id: 'id', name: 'name' } } },
        rules: { title: { required: 'insert', trim: true, min: 3, max: 120, message: 'A title of 3 to 120 characters' } },
        roles: { write: ['member', 'admin'] },
      },
      {
        as: 'users', table: 'users',
        columns: { id: 'id', name: 'name', email: 'email', password: 'password', role: 'role' },
        enums: { role: ['member', 'admin'] },
        hash: { password: hashPassword },
        hidden: ['password', 'email'],
        roles: { write: ['admin'] },
      },
    ],
  },
  seed: {
    users: [{ name: 'Ada', email: 'ada@example.com', password: 'change-me-please', role: 'admin' }],
  },
})
```

**The prompt** is read by the model with every request. Write it the way you
would explain the app to a colleague: what can be shown, who may do what. It
guides the plan; it does not grant anything — permissions are the schema's
`roles` and your route guards.

**The schema** is described in [schema.md](schema.md). Logical column names
(the keys of `columns`) are what the model reads; physical names are what the
database has. Keep them equal and in English when you can (`title`, not
`titre`): rows come back with physical names, and the model knows English
schema words best. Put the words people actually use in `says` and `columnSays`.

## 2. Pick a database

**None.** Leave `store` out: Talos opens `.talos/data.db` (or `TALOS_DB`), creates
every table and column the schema names, adds new ones on the next start and
never drops anything. `id` is the integer primary key, a column with declared
values defaults to the first one (`role: ['member', 'admin']` → `member`), and
`seed` rows go into tables that are still empty — hashed and defaulted as the
schema says. `await app.ready` resolves once the file is open.

**Yours.** Pass a store:

```ts
import { DatabaseSync } from 'node:sqlite'
import mysql from 'mysql2/promise'
import { sqliteStore, mysqlStore, syncSchema } from '@athenox/talos'

defineApp({ ..., store: sqliteStore(new DatabaseSync('app.db')) })          // or better-sqlite3
defineApp({ ..., store: mysqlStore(mysql.createPool(process.env.DATABASE_URL)) })
```

With a Drizzle schema, `fromDrizzle(schema)` gives the tables and
`syncSchema(store, schema)` creates what is missing (see [reference.md](reference.md)).

## 3. Serve it

### Nuxt / Nitro

```ts
// server/api/[...].ts
import { defineRoutes } from '@athenox/talos'

export default defineRoutes(app, {
  'GET    /api/recipes':     { say: 'show the recipes', orderBy: '-date', limit: 20, total: true },
  '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 },
})
```

`member`, `asMember` and `own` come from [accounts](accounts.md):

```ts
export const member = users.guard()                                              // the signed-in account, or 401
export const asMember = (me) => ({ userId: me.id, role: me.role })               // what Talos checks roles against
export const own = (_, me) => (me.role === 'admin' ? {} : { author_id: me.id })  // rows that are theirs
```

In `nuxt.config.ts`, keep Talos out of the bundle (it loads its model from its
own folder):

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

### Plain h3 / Node

```ts
import { createServer } from 'node:http'
import { createApp, toNodeListener } from 'h3'

const server = createApp()
server.use(defineRoutes(app, { /* the same table */ }))
createServer(toNodeListener(server)).listen(3000)
```

## 4. Check what it understood

Every answer carries the plan in its `x-talos-plan` header and the model call in
`x-talos-call`:

```
x-talos-plan: delete the recipe 12 => DELETE recipes FILTER id VAL 12 (compiled)
```

`(compiled)` means the plan was replayed without the model. The first call of a
route shows the model's plan — read it once when you add a route.

## Next

- [Routes](routes.md): templates, every option, free-text requests
- [Files and images](files.md): uploads
- [Telemetry and feedback](telemetry.md)
