# Lith
> Lith is a web language for AI agents: TypeScript + JSX + Prisma-style queries plus the declarations
> `table page api crud layout` and `state`/`effect`/`auth`. One global namespace, server/client split
> inferred, compiles to standard JS/HTML/CSS on Node. Status: v0 specification.
Everything below is the complete language card: read it once and you can write Lith.
Full specification (types, effects, codegen, every diagnostic code): https://lith.codes/llms-full.txt
## Language card
Lith = TypeScript + JSX + Prisma-style queries + the declarations `table page api crud layout` (at line start) and
`state`/`effect`/`auth`. All `.lith` files share one global namespace: never import your own files or Lith names; import
npm packages normally. JS built-ins (`Date`, `Set`, `fetch`, `setTimeout`, regex) work as in TS. Server code needs no
`await` (harmless).
## Tables
```tsx
table Post {
title: string(1..200) // refinement: length 1..200, checked on every write
body: string // any text, "" allowed
slug: string(1..80) unique // modifiers (unique, index, renamedFrom("old")) go anywhere after the base type
views: int(0..) = 0 // int(a..b) / number(a..b): value range
done: boolean = false
status: "Draft" | "Live" = "Draft"
due: day | null // day = "YYYY-MM-DD" (from ); compare with today()
author: User | null // reference: field authorId; include: { author: true } loads it
tags: Tag[] // many-to-many (join table is automatic)
created: Date = now()
}
```
- `id: int` is automatic; never declare it. `T | null` = optional. A new column with a default or `| null` needs no
migration. To rename a column and keep its data: `unitPriceCents: int(0..) renamedFrom("priceCents")`.
- `table Note own { ... }` adds `owner: User`, fills it on create and limits every query, update and delete to the
logged-in user's rows (others act as missing; login required). Use it only for private data. Shared rows with an owner
get `owner: User` plus checks in code. `system(() => ...)` runs code without `own` scoping; it is allowed only in
`auth(...)` pages/functions, `test` and `seed { }` blocks.
- `User` exists (`email: email unique`, `name: string`, `passwordHash`; `email` = text "@" text "." text). Add columns
with `table User { role: "Admin" | "Member" = "Member" }`. `passwordHash` never reaches the browser or JSON. Table
names may be any word (`Image`, `Event`).
## Queries (Prisma Client)
`db.post.findMany findFirst findUnique findUniqueOrThrow count create update delete updateMany deleteMany` with `where
data orderBy skip take include`; operators `equals not in contains startsWith endsWith lt lte gt gte AND OR NOT`
(`contains` and string sorting ignore case). `findUniqueOrThrow` → the row or a 404; `update`/`delete` of a missing row
→ 404. Nulls: `orderBy: [{ done: "asc" }, { due: { sort: "asc", nulls: "last" } }]`. Many-to-many:
`data: { tags: [tagA, tagB] }` sets the list, `where: { tags: { some: { name } } }` filters, `include: { tags: true }`.
A row loaded with `include` has those fields in its type: a `posts: Post[]` prop may read `p.tags` if the page loaded them.
## Pages and layout
```tsx
auth page "/posts/:id/:slug" (id: int, slug: string, q: string = "") { // path params typed and URL-decoded;
const post = db.post.findUniqueOrThrow({ where: { id } }) // other params come from the query string
if (slug !== post.slug) redirect(`/posts/${id}/${post.slug}`, 301) // default status 303
return {post.title}