# 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. This file holds the language card followed by the full specification. ## 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}

{post.title}

{post.created}

} layout { // optional; wraps every page; `children`, `me` and `path` are in scope return {children} } ``` `auth page` requires login (else redirect to `/login`); `auth(me.role === "Admin") page` also answers 403. `me` is the logged-in `User` or `null` (`User` under `auth`); `path` is the current URL path. A bad typed path param gives 404. Links are plain ``; a search form is `
`. A `Date` renders as `2026-10-16 14:05`. `` in `layout` forces or frees light/dark (built-in styles and Tailwind `dark:`). Stop anywhere with `redirect(path, status?)`, `notFound()`, `forbidden()`, `unauthorized()`, `fail(...)`. ## Server functions and forms A top-level function that uses `db`, `me`, `login`/`logout`/`loginWithPassword`/`hashPassword` or `process.env` runs on the server; the browser calls it automatically. `auth function` requires login (401). Parameters are typed and validated. ```tsx