init flags and add-ons
init flags: --framework <react|svelte> (default: react), --name <name>
(default: the folder name), --theme <id> / --radius <len> / --dark <mode> /
--font <id> / --font-mono <id> / --font-heading <id> / --icons <id>
(the look — see Theming), --ai <claude|codex|cursor|none>
(repeatable; writes CLAUDE.md/AGENTS.md/Cursor rules — none by default),
--marketplace <pkg> (repeatable, comma-separated; installs add-ons and
registers them — below), --force (scaffold into a non-empty folder),
--no-install (skip npm install), --web (choose all of the above in a
browser instead — see Choosing in a browser),
--from <source> (fill xano/ from an existing backend rather than the
starter — see Pulling an existing workspace).
In a terminal, init prompts for the framework and the AI files; every prompt
has a default, so pressing enter twice is a valid answer. The look is never
prompted for — it comes from the flags above, or from --web.
The starter backend is empty but already compiles and deploys — grow it from the
walkthrough in xano/EXAMPLE.md.
A scaffold ships @xanots/sdk and nothing else from the @xanots scope. Add-ons
install on demand:
init takes the same package names, so a project can be scaffolded with its
add-ons already wired:
xano/index.ts.
Installing alone would leave dependencies nothing imports — a different project
wearing the same name. A module declares how it registers in its own
package.json ("xanots": { "register": "registerAuth" }); one that has not
adopted the field is read for a single register* export, and two of those
without the field is a refusal rather than a guess.
@xanots/auth is authentication, not authorization — user/login/signup
tables and the endpoints over them. It ships no roles, permissions, or route
guards, so it is not the RBAC answer. Build role guards natively: a role column
on the auth table, then a s.precondition on each endpoint gating
auth("role").
details is the one to reach for when wiring an add-on: it prints the objects
the add-on puts on your workspace, what you have to supply, and the
xano/index.ts registration to copy. Piped, it emits JSON; --prompt emits
instructions written to be handed straight to a coding agent.
That is npm install with two additions: add-ons are discoverable from xanots --help, and the command refuses before npm runs when you are not standing in a
project — the mistake npm answers by silently writing to the wrong package.json.
The package name is passed through exactly as typed, so version specifiers, tags,
and third-party packages all work.
Choosing in a browser
--web collects the same choices against a live preview instead of on the
command line:
init with the flags your
choices imply — printing the equivalent command so the project stays
reproducible from a script. Everything after --web is passed to it untouched,
including --help, which is why that one form reaches the network. xanots help init stays offline, like init itself.
Because the configurator is fetched at run time, --web needs the npm registry
before it can start. init on its own reaches out only to install the new
project’s dependencies, which --no-install skips.
The frontend preset
To pointnpm run dev at a real backend, copy .env.example to .env.local — both
live at the project root, next to vite.config.ts — and set VITE_XANO_HOST to a
deployed URL. Deployed builds don’t need it: xanots deploy <entry> --static <dir> injects the
backend URL as window.XANO_HOST, which takes precedence.
The frontend ships Button and Card already vendored, plus a pre-configured
components.json, so npx shadcn@latest add dialog form input (or
npx shadcn-svelte@latest add … on a Svelte scaffold) works immediately — no
init step for either CLI. Components are copied into your repo rather than
installed, so you own and edit them directly. Lucide
is installed on both scaffolds — lucide-react on React, @lucide/svelte on
Svelte — and the landing page already uses it.
Theming
shadcn components carry no colors of their own: they are Tailwind utilities over a fixed set of semantic tokens (--primary, --muted-foreground, --border,
the --chart-* ramp, the --sidebar-* set). Those tokens live at the top of
frontend/src/index.css, which is the whole theme — Tailwind v4 keeps it in CSS,
and there is no tailwind.config.js. That one stylesheet backs both frameworks.
init renders it from a theme you choose, using shadcn’s own two-part model:
neutral (default), stone, zinc, mauve,
olive, mist, taupe. Accents — a partial override of primary, secondary,
the chart ramp, and the sidebar primary: amber, blue, cyan, emerald,
fuchsia, green, indigo, lime, orange, pink, purple, red, rose,
sky, teal, violet, yellow. The values are shadcn’s, verbatim, so
--theme zinc-blue is what ui.shadcn.com hands out for the same pair.
--theme also takes any shadcn registry theme — its own, a third-party
generator’s, or your team’s:
--radius <len> overrides the corner radius (a bare number is rem). Everything
else about the project is identical whichever theme you pick, and you can change
your mind later by editing the token values — or by applying another theme over
them with npx shadcn@latest add <registry-theme-url>.
Dark mode
Every theme ships a complete dark palette.--dark decides what turns it on:
system(default) — an inline script in the HTML entry applies the OS setting before first paint, so the page never flashes light first. No UI.toggle— that, plusfrontend/src/lib/theme.ts(the persisted mode) and a mode toggle on the landing page cycling system → light → dark.off— light only. The.darkblock is still there and still complete.
bg-primary,
text-muted-foreground) rather than raw palette classes like bg-gray-100:
raw ones ignore the theme and are unreadable in dark mode. The scaffolded AI
instruction files say so too.
Typefaces and icons
Fonts are opt-in and self-hosted: each choice installs an@fontsource
package rather than linking Google’s CDN, which would be a third-party request
on every page load of your app, a failure behind a firewall, and a privacy
question someone inherits later.
--font <id>— body text (Tailwind’s--font-sans, which v4 also uses as the page default). Sans faces only:geist,inter,figtree,manrope,dm-sans,space-grotesk,outfit, and 10 more.--font-mono <id>— code.jetbrains-monoorgeist-mono.--font-heading <id>— headings. Accepts any face, sans or serif, since a display serif over a sans body is the reason the slot exists. It emits a base-layer rule, so headings pick it up without touching every<h1>.- Omit a slot and it keeps Tailwind’s default stack, installing nothing for it.
init naming the slot it was resolving, so --font-mono inter is an error rather than a proportional face quietly rendering your code
blocks.
--icons <id> picks the icon set: lucide (default), tabler, or phosphor.
The binding covers the dark-mode toggle’s icons as well as the landing page’s —
without that, --icons tabler --dark toggle would emit a toggle importing a
library the project no longer installs, a build failure from a flag with nothing
to do with dark mode.
On a Svelte scaffoldnpm run typecheckrunssvelte-kit sync && svelte-checkrather thantsc. It checks the backend and the components together —tsccannot read.sveltefiles at all.
The SvelteKit scaffold prerenders every route.frontend/src/routes/+layout.tssetsprerender = true, so each route becomes its own HTML document at build time and loads as a real page. Pages live infrontend/src/routes/, andfilesin thesveltekit()plugin config keeps the project single-rooted withxano/as a peer. That config lives invite.config.ts— there is nosvelte.config.js, matching where SvelteKit’s own scaffold now puts it. There is still no server at runtime — Xano is the backend anddeploy --staticships to a host with no runtime, so+page.server.ts, form actions, and serverloadhave nothing to run on, and the build does not stop you. Treat them as unavailable rather than trusting a green build. Two things follow from prerendering. Because it renders at build time, module-scopewindow/documentaccess fails the build rather than the browser — useonMount, or guard withbrowser. And a dynamic route like/posts/[id]fails the build unless it declares which ids exist:That is deliberate — a loud build error beats shipping a page that 404s for real users. Unmatched paths get a real 404 fromfrontend/src/routes/404/+page.svelte, which prerenders to404.html. It has to be a route: SvelteKit never prerenders+error.svelteto a file, so that alone would ship no404.htmland every unknown path would serve the home page with a 200 instead. One more build-time check comes with prerendering: a hash link to an id that is not on the page it renders on —<a href="#pricing">with noid="pricing"— fails the build, naming the route and the id. A hash nav in+layout.svelteis exempt on/404only, since that route inherits the layout and by definition carries none of the page’s sections.