0B6OS Documentation
Single source of truth for setup, development, and deployment. This page is intentionally detailed so you can move from first run to production without guesswork.
Start Here
0B6OS is a SvelteKit starter template for Cloudflare. Create your own repository with Use this template, then run bun run customize to rename the
app, the slug, the dev port, and the Cloudflare resource names in one pass — CUSTOMIZE.md covers the steps a script cannot do.
Start locally with an isolated D1 database, then connect the production Cloudflare resources only when you are ready to deploy.
- Create your repository from the template and run the customization pass.
- Install the frozen Bun dependency graph.
- Apply database migrations locally.
- Run the app and verify the setup, login, CMS, and chat surfaces.
- Configure OAuth and owner access through the setup flow.
- Keep every coverage metric at or above the enforced 95% floor.
- Create project-owned Cloudflare bindings before building or deploying remotely.
Quick Start
Bun is the recommended default for this repository. Run the commands below, then open http://localhost:3160.
terminal bun
# Install dependencies
$
bun install
# Start development server
$
bun run dev
# Apply local D1 migrations
$
bun run db:migrate:local
# Build for production
$
bun run build
# Type-check and tests
$
bun run check
$
bun run test
# Coverage report
$
bun run test:coverage
# Deploy to Cloudflare Pages
$
bun run deploy
What You Get Out of the Box
0B6OS integrates its main product surfaces as one Cloudflare-native application: identity, content management, AI workflows, administration, analytics, and agent-ready publishing.
Core App Shell
- Keyboard-first navigation with command palette for fast route switching.
- Theme system with persistent light and dark preferences.
- Responsive layout, navigation, footer, and shared metadata components.
- A columned widget board with pointer, touch, and keyboard dragging — see Drag and Drop.
Account and Admin Flow
- Setup-first authentication flow for owner configuration.
- Login, signup, profile, reset, and admin routes are already scaffolded.
- Cloudflare D1 and KV are used for setup state and application data.
Content and AI Surfaces
- Private CMS content types (
isPublic: false) return404from public list/item routes and stay out of the sitemap; manage them under/admin/cms. - Chat exposes only enabled entries from its known model allowlist. Unknown or disabled models are rejected rather than forwarded to a provider.
- Voice transcripts and replies persist in the same conversation history as text chat.
Contact Abuse Protection
Turnstile is optional only when both keys are absent. Set TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY together to enable it;
partial configuration fails closed so the browser and server cannot disagree.
Analytics and Operations
- First-party, cookie-free analytics at
/admin/stats— traffic, audience, and growth, with no third-party script. - A Cloudflare plan-limit meter that projects whether today's traffic will exhaust your request allowance.
- Per-admin permission so operators can see stats without owner access.
How To Use the App
Use the app in this order if you want the least confusing first run. That sequence matches how the repo is structured and avoids most setup-related false alarms.
- Open
/setupfirst on a fresh environment and configure owner credentials. - Complete
/setupbefore expecting sign-in or AI features to work. - Sign in through
/auth/loginor create an account through/auth/signup. - Open the command palette with Ctrl/Cmd + K to move between major routes quickly.
- Use
/chatfor AI interactions,/profilefor account settings, and/adminfor operator tasks. - Use the theme toggle to verify light, dark, and system-preference presentation.
Common First-Run Checks
- If auth looks broken, re-check setup lock state and provider credentials first.
- If chat is missing from navigation, verify AI provider configuration and route access.
- If admin tools are unavailable, confirm you are signed in as the configured owner.
Where To Extend
- Modify routes under
src/routeswhen changing page behavior. - Use
src/lib/componentsfor reusable UI and shell elements. - Keep business logic in
src/lib/servicesand shared helpers insrc/lib/utils.
Drag and Drop
<WidgetBoard> takes a layout and a list of columns, and reports a new
layout through on:change. It stores nothing itself, so where a layout is saved
stays your decision.
<WidgetBoard bind:widgets {columns} on:change={(e) => save(e.detail.widgets)} /> Registering a widget takes three edits and none of them is the board: an entry in src/lib/widgets/manifest.ts, a line in src/lib/widgets/index.ts, and the component itself. The registry ships empty on
purpose. The two actions underneath the board — use:draggable and use:dropzone — work on any markup, so a sortable list or a nav reorder needs no board
at all.
Keyboard control
Every pointer gesture has a keyboard equivalent, announced through a live region. Focus a drag handle, then:
- Space or Enter — pick the widget up, and put it down again.
- Arrow up and down — move it within its column.
- Arrow left and right — move it to the column either side.
- Escape — cancel, returning it to where it started.
On a touchscreen, hold a handle briefly before dragging — a swipe stays a swipe, so the page still scrolls. Dragging near the top or bottom edge scrolls the page with you.
The rule that will bite you
A widget's stored state must be inert. A value that changes on a timer — a price, a clock, a
connection count — goes out through the widget's live event and comes back in
through the board's live prop; it must never be written into widget.title, which is persisted. Ignoring this is how a dashboard rewrites its
whole layout every thirty seconds and burns a day's storage quota from one open tab.
Full reference, including the reorder contract and the component checklist: docs/WIDGET_BOARD.md.
Working With AI in This Repo
Treat AI as a fast pair programmer, not as a source of truth. It is useful here because the repo already includes app structure, tests, and strong conventions, which gives the assistant real context to work against.
Good Prompts
- Point the assistant at a concrete file, route, failing test, or command.
- Ask it to write or update tests first when changing behavior.
- Ask for narrow fixes instead of broad rewrites unless you want architectural change.
Good Validation Habits
- Have the assistant explain which route, store, or service controls the behavior.
- Require executable validation after changes, not only a diff summary.
- Always finish by running check, tests, and coverage.
What AI Is Best At Here
- Tracing a route from UI to service layer and identifying the smallest edit surface.
- Adding tests around setup, auth, chat, or command palette behavior.
- Summarizing repo conventions such as Cloudflare bindings, migrations, and theme rules.
If you are using an AI coding agent, keep requests concrete: mention the page or failing test, state the desired behavior, and ask for the smallest validating change that solves it.
Core Commands
These scripts are defined in package.json and are the canonical local workflow.
Development
bun run devruns on host 0.0.0.0, port 3160.bun run previewpreviews the production build on port 3160.bun run checkruns Svelte sync plus svelte-check.
Testing
bun run testruns Vitest in CI mode.bun run test:watchruns Vitest in watch mode.bun run test:e2eruns Playwright tests.bun run test:allruns unit tests, then E2E tests.
Deploy and Validation
bun run deploybuilds then deploys .svelte-kit/cloudflare.bun run validate:contrastchecks theme contrast.bun run validate:allruns check + test + contrast validation.
Cloudflare Bindings
0B6OS is configured for Cloudflare Pages with these bindings in wrangler.toml:
DBas D1 database binding (database name: ob6os-db).KVas KV namespace for runtime config and flags.BUCKETas R2 bucket binding.- Queue producer binding is documented but commented out by default.
Set app secrets in Cloudflare dashboard or Wrangler secrets for production. Avoid committing raw secrets to source control.
Database Migrations
Migrations are ordered SQL files under migrations/ and tracked by D1. Never edit
or delete existing migration files once committed to main.
# Apply pending migrations to local D1
bun run db:migrate:local
# Apply pending migrations to remote D1
bun run db:migrate
# List migration status
bun run db:migrate:list When schema changes are needed, create a new file with the next sequence number (for
example, 0012_add_feature_flag.sql) and use ALTER TABLE or new CREATE statements.
Authentication and Setup Flow
Authentication is built into this app on a setup-first workflow — no third-party auth
library sits in the request path. The browser receives an unsigned opaque session token
(random, worthless without its server-side record — its SHA-256 digest is the lookup key),
while the revocable session record, identity, and roles are loaded from D1 on every request.
Authentication fails closed if D1 or SESSION_SECRET is unavailable.
You can sign in with email and password, or with GitHub and Discord once those providers are
configured. The main routes are /setup, /auth/login, /auth/signup, and /reset.
1. Configure
Set SESSION_SECRET and SETUP_SECRET, then open /setup and submit the bootstrap secret, GitHub OAuth credentials, and admin GitHub username.
2. Lock Setup
After owner/config state exists, only the authenticated owner can change setup or manage authentication keys. GitHub and Discord validate unexpired one-time state in D1, exchange the provider code, then atomically consume that state before changing accounts or sessions. Discord does not bootstrap ownership; it inherits owner status only when linked or matched to the configured GitHub owner account.
3. Reset When Needed
/reset is owner-only. It clears setup-related KV keys, revokes every active D1
session, and clears the browser cookie. The reset route can also be disabled.
Admin Analytics
/admin/stats is the built-in analytics surface: traffic over a 1, 7, 30, or 90-day
window, views by route, the most-read CMS items, referrers, countries, an audience breakdown,
and user and content growth over time. It is first-party — there is no third-party script, no
account, and no API key to provision.
Everything collected is a daily aggregate counter in D1. There are no cookies, no identifiers, and no IP addresses anywhere in the feature. The User-Agent is read to classify the request and then discarded, so only coarse buckets (operating system, browser, device, language, viewport) are ever stored, and country comes from the Cloudflare edge rather than from an IP lookup. Because nothing per-visitor is retained, this needs no consent banner.
Turning It On
- Apply migrations
0007through0009, plus0014for the Top content table, withdb:migrate:local(ordb:migratefor remote). - Collection starts on the next request. Traffic and audience panels fill in as visits arrive.
- Country stays
(unknown)in local development — it is supplied by the Cloudflare edge. - Top content appears once a published CMS item is read. It is the only counter whose row count grows with your catalogue rather than with a fixed list, so on a large catalogue watch it — the retention cron is what keeps it in hand.
Granting Access
The owner always sees Stats. Any other admin needs the can_view_stats flag, which
defaults to off so existing admins do not gain access on upgrade:
UPDATE users SET can_view_stats = 1 WHERE email = '[email protected]'; The permission is re-read from the database on every request, so revoking it takes effect without waiting for a sign-out.
Retention
The counter tables grow one row per day per dimension unless pruned. Set CRON_SECRET and have any scheduler POST to the retention endpoint; rows older
than 400 days are removed.
curl -X POST https://your-app/api/cron/prune-view-stats \
-H "Authorization: Bearer $CRON_SECRET"Platform Usage Meter
The same page tracks billable Function invocations — a larger set than page
views, since bots, /api/* calls, 404s, and non-GET requests all count against
your plan. It projects whether today will exhaust the free 100,000-request daily allowance
before the UTC reset.
Treat it as an early warning and a floor, not a bill: the Cloudflare dashboard remains authoritative.
Connecting Google Analytics
Optional, and off until you turn it on. The owner opens /admin/analytics, pastes either the GA4 Measurement ID (G-ABCD123456) or the whole gtag.js snippet — the ID is read out of it — and saves. A toggle
pauses the tag without discarding the ID, and Disconnect removes it entirely.
The tag loads on public pages only. /admin, /api, and /setup are excluded, so your own admin traffic stays out of the numbers, and
page views are sent on every client-side navigation rather than by gtag's automatic tracking,
which would count only the first page of a visit.
This is additive: the built-in stats keep working, and reports are read in Google's own console. Google Analytics sets cookies and collects per-visitor data, which is a different privacy posture from the built-in counters — review your privacy policy and add a consent banner where your jurisdiction requires one. Only the owner can change the connection.
Testing and Quality Gates
0B6OS follows Test-Driven Development. Write failing tests first, then implementation, then refactor.
# Run all tests
bun run test
# Run tests in watch mode
bun run test:watch
# Check coverage
bun run test:coverage
# Run E2E tests
bun run test:e2e
# Run all tests (unit + E2E)
bun run test:all Vitest enforces a 95% floor for lines, statements, functions, and branches. A change is not ready when any metric falls below that threshold.
Project Structure
0B6OS/
├── .github/ # Copilot and workflow instructions
├── src/
│ ├── lib/
│ │ ├── components/ # Reusable UI components
│ │ ├── services/ # Business logic
│ │ ├── stores/ # Svelte stores
│ │ ├── types/ # Shared type definitions
│ │ └── utils/ # Helpers
│ ├── routes/ # SvelteKit routes
│ │ ├── api/ # API endpoints
│ │ ├── auth/ # Authentication pages
│ │ ├── chat/ # Chat UI
│ │ ├── setup/ # First-time setup flow
│ │ └── documentation/ # This page
│ ├── app.css # Global styles & theme
│ └── app.html # HTML shell and install metadata
├── tests/ # unit/integration/e2e tests
├── migrations/ # Immutable D1 migration files
├── docs/ # Extended project docs
└── wrangler.toml # Cloudflare bindings/configDeployment to Cloudflare Pages
- Push your repository to GitHub.
- In Cloudflare dashboard, open Pages and connect the repository.
- Use build command
bun run build. - Use output directory
.svelte-kit/cloudflare. - Add D1, KV, and R2 bindings to the Pages project settings.
- Add required environment variables and secrets.
- Deploy and verify auth, setup flow, and database connectivity.
The local deploy script already uses wrangler pages deploy .svelte-kit/cloudflare.
Troubleshooting
- If setup API reports KV unavailable, create KV namespaces and update wrangler.toml binding IDs.
- If login fails after setup, confirm OAuth callback URL and ensure GitHub credentials are valid.
- If migrations fail, run
bun run db:migrate:listand check migration numbering. - If command palette entries are missing, verify AI provider status and authentication state.
Agent Readiness
This site publishes a machine-readable discovery layer so search crawlers and AI agents can
find it, read it efficiently, and understand how to interact with it. Everything below is
live without per-domain URL configuration. Placeholder Cloudflare bindings still make /api/health return 503 until project-owned D1/KV resources are configured.
- /robots.txt — crawl rules, explicit entries for AI crawlers (GPTBot, ClaudeBot, PerplexityBot and others), and Content Signals declaring how the content may be used.
- /sitemap.xml — every public page plus all published CMS content, regenerated on request so newly published items appear immediately.
- /.well-known/api-catalog — an RFC 9727 catalog of this deployment's APIs.
- /.well-known/agent-skills/index.json — short guides teaching an agent how to read content and contact the site, each with a SHA-256 digest.
- /auth.md — how agents authenticate (and what is not offered).
- /api/health — service health, used as the catalog's status link.
The catalog currently anchors these implemented endpoints: /api/contact-form-submissions, /api/health, /api/cms/types, /api/chat/models, and /api/chat/stream. Their catalog notes distinguish public, Turnstile-gated, and
session-authenticated access.
Reading pages as Markdown
Any page can be fetched as Markdown instead of HTML by sending an Accept: text/markdown header. Browsers are unaffected — HTML remains the
default. Responses include an x-markdown-tokens estimate so an agent can budget context
before reading.
curl -H 'Accept: text/markdown' https://0b6os.com/ In-browser tools (WebMCP)
When opened by a WebMCP-capable agent, this site registers tools for searching content, listing pages, reading a page as Markdown, navigating, and switching theme. They are read-and-navigate only and restricted to this site's own origin. The page-reading tool enforces the public sitemap allowlist with browser credentials omitted, so an authenticated visitor cannot expose private/admin pages through WebMCP.
Content usage policy
The shipped default is fully permissive — search=yes, ai-input=yes, ai-train=yes — which matches 0B6OS's public open-source content. Change CONTENT_SIGNAL in src/lib/agent-discovery.ts before publishing any proprietary
content; every robots.txt group picks the change up automatically.
DNS-based discovery (DNS-AID) is the one piece that must be added by hand, since DNS records
live with your provider rather than in this repo. See docs/AGENT_READINESS.md for the exact records and the DNSSEC requirement.
References
Contributing
Use TDD, keep changes small and reviewable, and run tests plus checks before opening a pull request.
- Write tests before implementation changes.
- Run bun run check, bun run test, and bun run test:coverage.
- Prefer Cloudflare-native services and minimal external dependencies.
- Do not edit past migration files; create a new one instead.