Project structure#
Janux is convention over configuration: every path below is optional and discovered by existence. Create the file and the feature turns on — there is nothing to register.
my-app/
src/
routes/ # file-system routes: index.tsx → /, orders/[id].tsx → /orders/:id
components/ # your components (static, islands, stores)
server/ # *.api.ts — server functions that double as agent tools
api/ # arbitrary HTTP handlers: src/api/upload.ts → POST /api/upload
client.ts # boot({ defs: [...] }) — the client entry
agent.ts # defineAgent({...}) — the copilot
stores.ts # exported store defs, so SSR can render store-dependent views
ctx.ts # (req) => ctx — per-request context: session, tenant, role
middleware.ts # runs before routing on every request
matchers.ts # custom typed route params: [post=slug]
i18n.ts # or src/i18n/index.ts — locales, messages, routing
styles.css # the app stylesheet, injected into the HTML shell
public/
favicon.svg # served and linked automatically
janux.config.ts # optional: title, output, llms.txt, path overrides
tsconfig.jsonWhat each convention does#
| Path | Turns on | Details |
|---|---|---|
src/routes/** |
Routing, layouts (_layout.tsx), groups ((name)/), dynamic and typed segments |
Navigation |
src/server/*.api.ts |
api() RPC endpoints, auto-published as agent tools |
api() as agent tools |
src/api/** |
Raw HTTP handlers by exported method (export function POST) and uploads |
HTTP handlers |
src/client.ts |
The client runtime (boot). Omit it for a zero-JS site |
SSR and resumability |
src/agent.ts |
The copilot at /_janux/agent + the embedded harness |
The agent and your copilot |
src/stores.ts |
Server-side store instances per request, so SSR can render views that read them | Stores |
src/ctx.ts |
The per-request ctx every route, intent, source and api() receives |
Auth and request context |
src/middleware.ts |
A request hook before routing — redirects, auth gates, locale hardening | Navigation § Middleware |
src/matchers.ts |
Custom param matchers (each export is (value: string) => boolean) |
Navigation § The route tree |
src/i18n.ts |
Locale-prefixed routing, typed t(), page-scoped client messages |
i18n |
src/styles.css |
Stylesheet link in the HTML shell (Tailwind works here) | Tailwind recipe |
public/favicon.svg |
Favicon link in the shell | — |
What the starter ships#
bun create janux my-app gives you the smallest app that shows both faces:
src/components/Counter.tsx— a bifacial island: schema state, an intent with a description, a view. This is the piece agents can read and drive.src/components/AgentPanel.tsx— the copilot UI, talking to/_janux/agent.src/routes/index.tsx— the page, withmetafor title and description.src/client.ts—boot({ defs: [Counter] }).src/components/Counter.test.ts— a unit test driving intents through the real pipeline.
Overriding the layout#
janux.config.ts takes the same options as the Vite plugin:
TSjanux.config.ts
import { defineConfig } from 'janux';
export default defineConfig({
title: 'My app',
output: 'static', // 'bun' (default) | 'static'
routesDir: 'src/pages', // if you dislike the defaults
serverDir: 'src/rpc',
clientEntry: 'src/entry.ts',
llmsTxt: { title: 'My app', description: 'What it does' },
});Edits are picked up live in dev. A "janux" field in package.json still works as a fallback, but janux.config.ts wins.
Next: Mental model · Editor setup