@suaveplan/demo
Unified Bun + Hono server with a React 19 SPA front-end and a JSON API
in one process. Demonstrates locale-prefix routing across five base
locales, @suaveplan/api-plugin-auth-driven Better-Auth authentication
backed by SurrealDB, and React + Tailwind v4 styling.
What's wired up
- Server — single Bun process running a Hono app on port
3300(override withPORT=…). The Hono instance is the one owned by aSuaveAppfrom@suaveplan/api./api/healthis registered byregisterHealthRoutes;/api/auth/*is mounted by@suaveplan/api-plugin-auth(9 OpenAPI routes + the Better-Auth catch-all);/api/meis a demo convenience that callsauthService.getSession(headers); everything else falls through to the Vite-built client bundle indist/client/, withindex.htmlfallback so the client router owns deep links. - Auth — the canonical genesis pipeline. The Hono app is assembled
via
defineApp()from@suaveplan/api, withcreateAuthPlugin(...)from@suaveplan/api-plugin-authregistered as a plugin. The auth plugin's 6-steponInitapplies the bundled 17-table Better-Auth SurrealQL schema, constructs the real Better-Auth instance backed bybetterAuthSurrealAdapter(from@suaveplan/better-auth-surrealdb, the Better-Auth v1.6DBAdapterInstancefactory), registers the typedauthServiceon the hostServiceRegistry, mounts the 9 OpenAPI routes plus the Better-Auth catch-all under/api/auth/*, and installs the global session-resolver middleware so every request gets a typedPrincipalon its Hono context. A small inlinedemo-db-seedplugin (insrc/server/auth.ts) drops the liveSurrealAdapterintoServiceRegistry["db"]duringonRegisterso the auth plugin can resolve it duringonInit. Backing store is an external SurrealDB reached viaSURREAL_URL(defaultws://localhost:8000), namespaceSURREAL_NS(defaultdemo), databaseSURREAL_DB(defaultdemo), credentialsSURREAL_USER/SURREAL_PASS(defaultroot/root). - Client — React 19 + Vite +
@tailwindcss/vitefor Tailwind v4. Routing viareact-router-dom@7with a locale-prefix design. The app root composes<ReactUiProvider>(from@suaveplan/react-ui) around the entire tree, seeded with a singletonI18nInstance(createI18nfrom@suaveplan/i18n) and the chassislightThemeplugin.<ReactUiProvider>internally wraps<TranslationProvider>→<ThemeProvider>→<PackRegistryProvider>and auto-registers the matching chassis locale bundle on mount / language change, so any react-ui primitive that callsuseTranslation(...)finds a populated catalog. Form pages (/auth/login,/auth/register,/auth/forgot-password,/user/settings) compose<Card>,<Field>,<FieldLabel>,<Input>,<Button>,<Alert>from the chassis — no hand-rolled Tailwind on those primitives. Demo-specific copy continues to flow through a typedLocaleProviderso the page-level message catalogs stay co-located and statically checked; the i18n instance is driven to the matching BCP47 variant (en→en-GB,zh→zh-CN,ar→ar-AE,cy→cy-GB,ru→ru-RU) viasyncDemoLocale(base)from auseEffect.
Locale-prefix routing
Five base locales: en, zh, ar, cy, ru. Region variants
(e.g. en-GB, zh-CN, ar-EG) parse case-insensitively and
canonicalise to lowercase-base + UPPERCASE-region. Anything else
renders the NotFoundPage with a language picker.
/→Navigateto/<detected>/home(usesnavigator.languages)./<locale>/home/<locale>/about-us/<locale>/contact-us/<locale>/privacy-policy/<locale>/terms-of-service/<locale>/auth/login/<locale>/auth/register/<locale>/auth/forgot-password/<locale>/auth/logout/<locale>/user/control-panel(gated —RequireAuth)/<locale>/user/settings(gated —RequireAuth)/<locale>/admin/control-panel(gated —RequireRole admin)/<locale>/admin/settings(gated —RequireRole admin)
ar flips <html dir="rtl">. The <LocaleSwitcher> rewrites the URL
in place when a user changes language.
Running the demo
The demo requires a reachable SurrealDB instance. The simplest path:
docker run -d --name demo-surreal -p 8000:8000 \
surrealdb/surrealdb:latest start --user root --pass root --bind 0.0.0.0:8000
Then, from the repo root:
bun install # workspace deps
bunx turbo build --filter=@suaveplan/demo # build api/auth deps + dist/client
bun run --filter=@suaveplan/demo dev # boot the server on :3300
Visit http://localhost:3300 — it should redirect to /en/home (or
your browser's preferred locale if it matches one of the five).
Smoke endpoints:
curl http://localhost:3300/api/health
curl http://localhost:3300/api/me
curl http://localhost:3300/api/auth/get-session
End-to-end sign-up roundtrip — produces a 200 with a Better-Auth session token + user record:
curl -X POST http://localhost:3300/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"hunter2","name":"Alice"}'
Env overrides (all optional):
| Variable | Default |
|---|---|
PORT |
3300 |
SURREAL_URL |
ws://localhost:8000 |
SURREAL_NS |
demo |
SURREAL_DB |
demo |
SURREAL_USER |
root |
SURREAL_PASS |
root |
BETTER_AUTH_URL |
http://localhost:3300 |
BETTER_AUTH_SECRET |
demo placeholder — set in prod |