bosia 0.7.3 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/cli/create.ts +1 -0
  3. package/src/core/client/App.svelte +14 -2
  4. package/src/core/client/prefetch.ts +43 -1
  5. package/src/core/dev.ts +10 -2
  6. package/src/core/renderer.ts +15 -9
  7. package/src/core/server.ts +39 -1
  8. package/templates/store/.env.example +12 -0
  9. package/templates/store/.prettierignore +7 -0
  10. package/templates/store/.prettierrc.json +9 -0
  11. package/templates/store/README.md +62 -0
  12. package/templates/store/_gitignore +15 -0
  13. package/templates/store/bosia.config.ts +10 -0
  14. package/templates/store/instructions.txt +8 -0
  15. package/templates/store/package.json +26 -0
  16. package/templates/store/public/favicon.svg +14 -0
  17. package/templates/store/public/logo-dark.svg +14 -0
  18. package/templates/store/public/logo-light.svg +14 -0
  19. package/templates/store/src/app.css +140 -0
  20. package/templates/store/src/app.d.ts +14 -0
  21. package/templates/store/src/app.html +11 -0
  22. package/templates/store/src/hooks.server.ts +21 -0
  23. package/templates/store/src/lib/utils.ts +1 -0
  24. package/templates/store/src/routes/(private)/+layout.server.ts +10 -0
  25. package/templates/store/src/routes/(private)/+layout.svelte +44 -0
  26. package/templates/store/src/routes/(private)/dashboard/+page.svelte +11 -0
  27. package/templates/store/src/routes/(public)/+layout.svelte +13 -0
  28. package/templates/store/src/routes/(public)/+page.svelte +38 -0
  29. package/templates/store/src/routes/+error.svelte +19 -0
  30. package/templates/store/src/routes/+layout.server.ts +9 -0
  31. package/templates/store/src/routes/+layout.svelte +6 -0
  32. package/templates/store/template.json +11 -0
  33. package/templates/store/tsconfig.json +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.7.3",
3
+ "version": "0.7.6",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/cli/create.ts CHANGED
@@ -16,6 +16,7 @@ const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
16
16
  default: "Minimal starter with routing and Tailwind",
17
17
  demo: "Full-featured demo with hooks, API routes, form actions, and more",
18
18
  shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
19
+ store: "Online store starter (Postgres + MinIO/S3) with auth, RBAC, products/orders/cart",
19
20
  };
20
21
 
21
22
  export async function runCreate(name: string | undefined, args: string[] = []) {
@@ -3,7 +3,7 @@
3
3
  import { router, scrollToHash } from "./router.svelte.ts";
4
4
  import { findMatch } from "../matcher.ts";
5
5
  import { clientRoutes } from "bosia:routes";
6
- import { consumePrefetch, prefetchCache, dataUrl } from "./prefetch.ts";
6
+ import { consumePrefetch, prefetchCache, dataUrl, buildParentSnapshots } from "./prefetch.ts";
7
7
  import { appState, clearDirty } from "./appState.svelte.ts";
8
8
  import { captureSnapshot, liveContext, shouldRerun, type CacheEntry } from "./loaderCache.ts";
9
9
  import { pickErrorPage } from "../errorMatch.ts";
@@ -123,10 +123,22 @@
123
123
  // to avoid a flash of stale/empty data before the fetch completes.
124
124
  const cached = match.route.hasServerData ? consumePrefetch(path) : null;
125
125
  prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
126
+ // Forward cached parent data for skipped layers so downstream loaders see
127
+ // real parent() data, not {}. POST only when there's something to carry —
128
+ // keeps the no-skip case a cacheable/dedupable GET.
129
+ const snapshots = buildParentSnapshots(path, maskBits);
130
+ const dataInit: RequestInit =
131
+ Object.keys(snapshots).length > 0
132
+ ? {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({ parentSnapshots: snapshots }),
136
+ }
137
+ : {};
126
138
  const dataFetch = cached
127
139
  ? Promise.resolve(cached)
128
140
  : match.route.hasServerData
129
- ? fetch(dataUrl(path, maskBits))
141
+ ? fetch(dataUrl(path, maskBits), dataInit)
130
142
  .then((r) => r.json())
131
143
  .catch(() => null)
132
144
  : Promise.resolve(null);
@@ -38,6 +38,36 @@ export function buildMaskBits(path: string): string | null {
38
38
  return (pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
39
39
  }
40
40
 
41
+ /**
42
+ * Build `parentSnapshots` (layout depth → cached data) for a target path from
43
+ * the current loader cache, given the mask bits from `buildMaskBits`. For each
44
+ * layout depth whose mask bit is '0' (skipped) and whose cached entry exists,
45
+ * forward that layer's data so server-side downstream loaders see real
46
+ * `parent()` data instead of `{}`. Returns `{}` when nothing to carry.
47
+ *
48
+ * Client-supplied perf hint only — the server never trusts it for authz.
49
+ */
50
+ export function buildParentSnapshots(
51
+ path: string,
52
+ maskBits: string,
53
+ ): Record<number, Record<string, any>> {
54
+ const snapshots: Record<number, Record<string, any>> = {};
55
+ const url = new URL(path, window.location.origin);
56
+ const match = findMatch(clientRoutes, url.pathname);
57
+ if (!match) return snapshots;
58
+ const layoutIds = (match.route as any).layoutIds as (string | null)[];
59
+
60
+ layoutIds.forEach((id, depth) => {
61
+ // maskBits char 0 = page, char depth+1 = layout depth. '0' = skipped.
62
+ if (maskBits[depth + 1] !== "0") return;
63
+ if (id === null) return;
64
+ const entry = appState.loaderCache.layouts[id];
65
+ if (entry) snapshots[depth] = entry.data;
66
+ });
67
+
68
+ return snapshots;
69
+ }
70
+
41
71
  /** Builds the `/__bosia/data/…` URL for a given client path. */
42
72
  export function dataUrl(path: string, invalidatedBits?: string): string {
43
73
  const url = new URL(path, window.location.origin);
@@ -78,7 +108,19 @@ export async function prefetchPath(path: string): Promise<void> {
78
108
  // loaders whose tracked inputs haven't changed. Falls back to running
79
109
  // everything when the route can't be matched (e.g. external/unknown URL).
80
110
  const maskBits = buildMaskBits(path) ?? undefined;
81
- const res = await fetch(dataUrl(path, maskBits));
111
+ // Forward cached parent data for skipped layers so a prefetched response
112
+ // is computed with real parent() data, not {}. POST only when there's
113
+ // something to carry — keeps the no-skip case a cacheable/dedupable GET.
114
+ const snapshots = maskBits ? buildParentSnapshots(path, maskBits) : {};
115
+ const init: RequestInit =
116
+ Object.keys(snapshots).length > 0
117
+ ? {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ parentSnapshots: snapshots }),
121
+ }
122
+ : {};
123
+ const res = await fetch(dataUrl(path, maskBits), init);
82
124
  if (res.ok) {
83
125
  if (prefetchCache.size >= MAX_PREFETCH_ENTRIES) {
84
126
  const oldest = prefetchCache.keys().next().value;
package/src/core/dev.ts CHANGED
@@ -346,9 +346,17 @@ const devServer = Bun.serve({
346
346
  target.hostname = "127.0.0.1";
347
347
  target.port = String(APP_PORT);
348
348
 
349
+ // Preserve an X-Forwarded-* set by an OUTER proxy (e.g. a multi-tenant host
350
+ // fronting `bun run dev` behind TLS). Overwriting it with this dev proxy's
351
+ // own loopback host/scheme would strip the real public origin, so the app's
352
+ // redirects and `event.url` would point at localhost. Fall back to this
353
+ // proxy's request only when the outer hop didn't set them.
349
354
  const forwardedHeaders = new Headers(req.headers);
350
- forwardedHeaders.set("x-forwarded-host", reqUrl.host);
351
- forwardedHeaders.set("x-forwarded-proto", reqUrl.protocol.replace(":", ""));
355
+ forwardedHeaders.set("x-forwarded-host", req.headers.get("x-forwarded-host") ?? reqUrl.host);
356
+ forwardedHeaders.set(
357
+ "x-forwarded-proto",
358
+ req.headers.get("x-forwarded-proto") ?? reqUrl.protocol.replace(":", ""),
359
+ );
352
360
  // Force inner app to respond uncompressed. Bun's `fetch()` auto-decodes
353
361
  // gzip/br bodies but leaves the original `Content-Encoding` header on
354
362
  // the Response, so passing it through made Safari throw -1015 ("cannot
@@ -296,10 +296,15 @@ function makeDepends(deps: LoaderDeps): (...keys: string[]) => void {
296
296
  // - layouts[i] === true → run that layout; false → skip, emit null
297
297
  // - page === true → run page; false → skip, emit null
298
298
  // When skipped, the parent() chain still receives the *combined parent
299
- // data* contributed by previously-cached layers, which the client
300
- // reconstructs and forwards in the request body. For now (initial
301
- // implementation), skipped loaders contribute `{}` to parent and the
302
- // response slot is `null`; the client merges with its cached data.
299
+ // data* contributed by previously-cached layers. The client already holds
300
+ // each skipped layer's data in its loader cache and forwards it as
301
+ // `parentSnapshots` (depth data) in the request body, so downstream
302
+ // loaders that DO re-run see real parent() data, not `{}`. The response
303
+ // slot stays `null` (client renders that layer from its cache).
304
+ //
305
+ // Trust boundary: parentSnapshots are a client-supplied perf hint, never
306
+ // authoritative. Anything authz-related must read `event.locals` (populated
307
+ // in hooks.server.ts), never `parent()`.
303
308
 
304
309
  export type LoaderMask = {
305
310
  page: boolean;
@@ -314,6 +319,7 @@ export async function loadRouteData(
314
319
  metadataData: Record<string, any> | null = null,
315
320
  match?: RouteMatch<(typeof serverRoutes)[number]> | null,
316
321
  mask?: LoaderMask,
322
+ parentSnapshots?: Record<number, Record<string, any>>,
317
323
  ) {
318
324
  match ??= findMatch(serverRoutes, url.pathname);
319
325
  if (!match) return null;
@@ -332,11 +338,11 @@ export async function loadRouteData(
332
338
  if (skip) {
333
339
  layoutData[ls.depth] = null;
334
340
  layoutDeps[ls.depth] = null;
335
- // Skipped layers contribute {} to the parent chain. The client
336
- // already has their data and renders it from cache, so dependent
337
- // loaders that DO re-run will see stale parent() data here. This
338
- // is the same trade-off SvelteKit makes; loaders that need fresh
339
- // upstream data should call `depends()` on a shared key.
341
+ // Skipped layers contribute their client-cached data (forwarded as
342
+ // parentSnapshots) to the parent chain, so downstream loaders that DO
343
+ // re-run see real parent() data. Falls back to {} when no snapshot was
344
+ // sent. Perf hint only never authoritative for authz (use locals).
345
+ parentData = { ...parentData, ...(parentSnapshots?.[ls.depth] ?? {}) };
340
346
  continue;
341
347
  }
342
348
  const mod = await ls.loader();
@@ -237,8 +237,32 @@ async function resolve(event: RequestEvent): Promise<Response> {
237
237
  pageMatch?.route ? ((pageMatch.route as any).layoutModules?.length ?? 0) : 0,
238
238
  )
239
239
  : undefined;
240
+ // Client forwards each skipped layout layer's cached data as
241
+ // parentSnapshots (depth → data) so downstream loaders see real
242
+ // parent() data instead of {}. Perf hint only — never authoritative;
243
+ // authz must read locals. Guarded: undefined for GET / malformed body.
244
+ let parentSnapshots: Record<number, Record<string, any>> | undefined;
245
+ if (method !== "GET") {
246
+ try {
247
+ const body = await request.json();
248
+ if (body && typeof body === "object" && body.parentSnapshots) {
249
+ parentSnapshots = body.parentSnapshots as Record<number, Record<string, any>>;
250
+ }
251
+ } catch {
252
+ parentSnapshots = undefined;
253
+ }
254
+ }
240
255
  const runLoad = async () => {
241
- const data = await loadRouteData(routeUrl, locals, request, cookies, null, pageMatch, mask);
256
+ const data = await loadRouteData(
257
+ routeUrl,
258
+ locals,
259
+ request,
260
+ cookies,
261
+ null,
262
+ pageMatch,
263
+ mask,
264
+ parentSnapshots,
265
+ );
242
266
 
243
267
  let metadata = null;
244
268
  if (pageMatch) {
@@ -757,6 +781,20 @@ if (_xfoDisabled) {
757
781
  }
758
782
 
759
783
  async function handleRequest(request: Request, url: URL): Promise<Response> {
784
+ // Behind a trusted proxy the inbound `Host`/scheme is the proxy's inner hop
785
+ // (e.g. `localhost:PORT` over plain HTTP), so `url` built from `request.url`
786
+ // misreports the public origin. Rebuild it from `X-Forwarded-Host`/`-Proto`
787
+ // so `event.url` — and every absolute redirect, canonical URL, and
788
+ // `url.origin` the app derives — points at the public-facing origin instead
789
+ // of localhost. Gated on TRUST_PROXY since these headers are client-spoofable
790
+ // when no proxy strips them.
791
+ if (TRUST_PROXY) {
792
+ const fwdHost = request.headers.get("x-forwarded-host");
793
+ if (fwdHost) url.host = fwdHost;
794
+ const fwdProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
795
+ if (fwdProto) url.protocol = `${fwdProto}:`;
796
+ }
797
+
760
798
  // Reject new non-health requests during shutdown
761
799
  if (shuttingDown && url.pathname !== "/_health") {
762
800
  return new Response("Service Unavailable", {
@@ -0,0 +1,12 @@
1
+ DATABASE_URL=postgres://postgres:postgres@localhost:5432/store
2
+
3
+ SESSION_SECRET=change-me-in-production
4
+
5
+ STORAGE_DRIVER=s3
6
+ S3_BUCKET=uploads
7
+ S3_REGION=auto
8
+ S3_ACCESS_KEY_ID=minioadmin
9
+ S3_SECRET_ACCESS_KEY=minioadmin
10
+ S3_ENDPOINT=http://localhost:9000
11
+
12
+ PUBLIC_BASE_URL=
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ .bosia
5
+ bun.lock
6
+ public/bosia-tw.css
7
+ bosia.json
@@ -0,0 +1,9 @@
1
+ {
2
+ "useTabs": true,
3
+ "tabWidth": 2,
4
+ "singleQuote": false,
5
+ "trailingComma": "all",
6
+ "printWidth": 100,
7
+ "plugins": ["prettier-plugin-svelte"],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
@@ -0,0 +1,62 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ An online-store starter built with [Bosia](https://github.com/bosapi/bosia) — Postgres database, MinIO (S3) uploads, auth, RBAC, and the shop domain (products / orders / cart).
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) v1.1+
8
+ - PostgreSQL running locally or remotely
9
+ - An S3-compatible bucket (AWS S3, Cloudflare R2, MinIO, ...)
10
+
11
+ ## Getting Started
12
+
13
+ ```bash
14
+ cp .env.example .env
15
+ # fill DATABASE_URL, SESSION_SECRET, and S3_* in .env
16
+
17
+ bun run db:generate
18
+ bun run db:migrate
19
+ bun run db:seed
20
+
21
+ bun x bosia dev
22
+ ```
23
+
24
+ Visit [http://localhost:9000](http://localhost:9000). The **first account you register becomes the admin** (gets `('*','*')` via the RBAC bootstrap seed).
25
+
26
+ ## What ships
27
+
28
+ | Feature | Path |
29
+ | ------------- | ---------------------------------------------------------------------- |
30
+ | `auth` | `src/features/auth/`, `(public)/login`, `(public)/register`, `/logout` |
31
+ | `rbac` | `src/features/rbac/`, `locals.can(r,a,scope?)` |
32
+ | `file-upload` | `src/features/file-upload/`, `POST /api/files` (S3 via `Bun.s3`) |
33
+ | `shop` | `src/features/shop/` (products / orders / cart services) |
34
+
35
+ ## Routes
36
+
37
+ - `/` — public landing
38
+ - `/login`, `/register`, `POST /logout`
39
+ - `/dashboard` — gated; redirects to `/login` if unauthenticated
40
+
41
+ ## Scripts
42
+
43
+ | Command | Description |
44
+ | --------------------- | --------------------------------------------- |
45
+ | `bun x bosia dev` | Dev server with HMR |
46
+ | `bun x bosia build` | Production build |
47
+ | `bun run db:generate` | Generate migration from schema changes |
48
+ | `bun run db:migrate` | Apply pending migrations |
49
+ | `bun run db:seed` | Run pending seed files (incl. RBAC bootstrap) |
50
+
51
+ ## S3 storage
52
+
53
+ Uses native `Bun.s3` (no `@aws-sdk/*` dependency). Defaults target a local MinIO; point `S3_ENDPOINT` at any S3-compatible store (AWS S3, Cloudflare R2, ...):
54
+
55
+ ```
56
+ STORAGE_DRIVER=s3
57
+ S3_BUCKET=uploads
58
+ S3_REGION=auto
59
+ S3_ACCESS_KEY_ID=minioadmin
60
+ S3_SECRET_ACCESS_KEY=minioadmin
61
+ S3_ENDPOINT=http://localhost:9000 # MinIO; omit for AWS S3
62
+ ```
@@ -0,0 +1,15 @@
1
+ node_modules/
2
+ dist/
3
+ .bosia/
4
+ .DS_Store
5
+ *.log
6
+
7
+ # Generated Tailwind output
8
+ public/bosia-tw.css
9
+
10
+ # Local env overrides — never commit secrets
11
+ .env*.local
12
+ .env
13
+
14
+ # SQLite database artifacts
15
+ data/*.db*
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });
@@ -0,0 +1,8 @@
1
+ Update .env with your DATABASE_URL (Postgres, e.g. postgres://user:pass@localhost:5432/store) and S3_* credentials (MinIO or any S3-compatible store).
2
+ Pick a strong SESSION_SECRET.
3
+
4
+ bun run db:generate
5
+ bun run db:migrate
6
+ bun run db:seed
7
+
8
+ The first account you register becomes the admin.
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bosia dev",
7
+ "build": "bosia build",
8
+ "start": "bosia start",
9
+ "check": "tsc --noEmit && prettier --check .",
10
+ "format": "prettier --write .",
11
+ "format:check": "prettier --check ."
12
+ },
13
+ "dependencies": {
14
+ "bosia": "^{{BOSIA_VERSION}}",
15
+ "svelte": "^5.56.3",
16
+ "tailwind-merge": "^3.5.0",
17
+ "drizzle-orm": "^0.44.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "prettier": "^3.3.0",
22
+ "prettier-plugin-svelte": "^3.2.0",
23
+ "typescript": "^5",
24
+ "drizzle-kit": "^0.31.0"
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="currentColor" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="currentColor" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="currentColor" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="currentColor" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="currentColor" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Top block -->
3
+ <rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
4
+ <rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
5
+
6
+ <!-- Middle block -->
7
+ <rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
8
+
9
+ <!-- Bottom block -->
10
+ <rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
11
+
12
+ <!-- Connector bar on left -->
13
+ <rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
14
+ </svg>
@@ -0,0 +1,140 @@
1
+ @import "tailwindcss";
2
+ @source "../src";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *));
5
+
6
+ /*
7
+ * ─── shadcn-inspired Design Tokens ──────────────────────
8
+ * CSS custom properties for light & dark themes.
9
+ * Uses HSL values so Tailwind can apply opacity modifiers.
10
+ */
11
+
12
+ @theme {
13
+ --color-background: hsl(var(--background));
14
+ --color-foreground: hsl(var(--foreground));
15
+
16
+ --color-card: hsl(var(--card));
17
+ --color-card-foreground: hsl(var(--card-foreground));
18
+
19
+ --color-popover: hsl(var(--popover));
20
+ --color-popover-foreground: hsl(var(--popover-foreground));
21
+
22
+ --color-primary: hsl(var(--primary));
23
+ --color-primary-foreground: hsl(var(--primary-foreground));
24
+
25
+ --color-secondary: hsl(var(--secondary));
26
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
27
+
28
+ --color-muted: hsl(var(--muted));
29
+ --color-muted-foreground: hsl(var(--muted-foreground));
30
+
31
+ --color-accent: hsl(var(--accent));
32
+ --color-accent-foreground: hsl(var(--accent-foreground));
33
+
34
+ --color-destructive: hsl(var(--destructive));
35
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
36
+
37
+ --color-border: hsl(var(--border));
38
+ --color-input: hsl(var(--input));
39
+ --color-ring: hsl(var(--ring));
40
+
41
+ --radius-sm: calc(var(--radius) - 4px);
42
+ --radius-md: calc(var(--radius) - 2px);
43
+ --radius-lg: var(--radius);
44
+ --radius-xl: calc(var(--radius) + 4px);
45
+ }
46
+
47
+ /* bosia-theme-vars: template default tokens. `bosia add theme` strips everything
48
+ between these markers (the installed theme owns these tokens). Do NOT add your
49
+ own :root rules inside the markers — put custom overrides after the close tag. */
50
+
51
+ /* ─── Light Theme (Default) ─────────────────────────────── */
52
+
53
+ :root {
54
+ --background: 0 0% 100%;
55
+ --foreground: 222.2 84% 4.9%;
56
+
57
+ --card: 0 0% 100%;
58
+ --card-foreground: 222.2 84% 4.9%;
59
+
60
+ --popover: 0 0% 100%;
61
+ --popover-foreground: 222.2 84% 4.9%;
62
+
63
+ --primary: 222.2 47.4% 11.2%;
64
+ --primary-foreground: 210 40% 98%;
65
+
66
+ --secondary: 210 40% 96.1%;
67
+ --secondary-foreground: 222.2 47.4% 11.2%;
68
+
69
+ --muted: 210 40% 96.1%;
70
+ --muted-foreground: 215.4 16.3% 46.9%;
71
+
72
+ --accent: 210 40% 96.1%;
73
+ --accent-foreground: 222.2 47.4% 11.2%;
74
+
75
+ --destructive: 0 84.2% 60.2%;
76
+ --destructive-foreground: 210 40% 98%;
77
+
78
+ --border: 214.3 31.8% 91.4%;
79
+ --input: 214.3 31.8% 91.4%;
80
+ --ring: 222.2 84% 4.9%;
81
+
82
+ --radius: 0.5rem;
83
+ }
84
+
85
+ /* ─── Dark Theme ─────────────────────────────────────────── */
86
+
87
+ .dark {
88
+ --background: 222.2 84% 4.9%;
89
+ --foreground: 210 40% 98%;
90
+
91
+ --card: 222.2 84% 4.9%;
92
+ --card-foreground: 210 40% 98%;
93
+
94
+ --popover: 222.2 84% 4.9%;
95
+ --popover-foreground: 210 40% 98%;
96
+
97
+ --primary: 210 40% 98%;
98
+ --primary-foreground: 222.2 47.4% 11.2%;
99
+
100
+ --secondary: 217.2 32.6% 17.5%;
101
+ --secondary-foreground: 210 40% 98%;
102
+
103
+ --muted: 217.2 32.6% 17.5%;
104
+ --muted-foreground: 215 20.2% 65.1%;
105
+
106
+ --accent: 217.2 32.6% 17.5%;
107
+ --accent-foreground: 210 40% 98%;
108
+
109
+ --destructive: 0 62.8% 30.6%;
110
+ --destructive-foreground: 210 40% 98%;
111
+
112
+ --border: 217.2 32.6% 17.5%;
113
+ --input: 217.2 32.6% 17.5%;
114
+ --ring: 212.7 26.8% 83.9%;
115
+ }
116
+
117
+ /* /bosia-theme-vars */
118
+
119
+ /* ─── Base Styles ────────────────────────────────────────── */
120
+
121
+ @layer base {
122
+ * {
123
+ border-color: theme(--color-border);
124
+ }
125
+
126
+ body {
127
+ background-color: theme(--color-background);
128
+ color: theme(--color-foreground);
129
+ font-family:
130
+ "Inter",
131
+ system-ui,
132
+ -apple-system,
133
+ BlinkMacSystemFont,
134
+ "Segoe UI",
135
+ Roboto,
136
+ "Helvetica Neue",
137
+ Arial,
138
+ sans-serif;
139
+ }
140
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="svelte" />
2
+
3
+ declare module "*.svelte" {
4
+ import type { Component } from "svelte";
5
+ const component: Component<Record<string, any>, Record<string, any>, any>;
6
+ export default component;
7
+ }
8
+
9
+ declare namespace App {
10
+ interface Locals {
11
+ db: import("./features/drizzle").Database;
12
+ requestTime: number;
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="%bosia.lang%">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ %bosia.head%
7
+ </head>
8
+ <body>
9
+ %bosia.body%
10
+ </body>
11
+ </html>
@@ -0,0 +1,21 @@
1
+ import { sequence } from "bosia";
2
+ import type { Handle } from "bosia";
3
+ import { db } from "./features/drizzle";
4
+ import { authHandle } from "./features/auth";
5
+
6
+ const dbHandle: Handle = async ({ event, resolve }) => {
7
+ event.locals.db = db;
8
+ return resolve(event);
9
+ };
10
+
11
+ const loggingHandle: Handle = async ({ event, resolve }) => {
12
+ const start = Date.now();
13
+ event.locals.requestTime = start;
14
+ const res = await resolve(event);
15
+ const ms = Date.now() - start;
16
+ console.log(`[${event.request.method}] ${event.url.pathname} ${res.status} (${ms}ms)`);
17
+ res.headers.set("X-Response-Time", `${ms}ms`);
18
+ return res;
19
+ };
20
+
21
+ export const handle = sequence(dbHandle, authHandle, loggingHandle);
@@ -0,0 +1 @@
1
+ export { cn } from "bosia";
@@ -0,0 +1,10 @@
1
+ import { redirect } from "bosia";
2
+ import type { LoadEvent } from "bosia";
3
+
4
+ export async function load({ locals, url }: LoadEvent) {
5
+ if (!locals.user) {
6
+ const next = encodeURIComponent(url.pathname + url.search);
7
+ throw redirect(303, `/login?next=${next}`);
8
+ }
9
+ return { user: locals.user };
10
+ }
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import { page } from "bosia/client";
3
+ import AdminSidebar from "$lib/components/AdminSidebar.svelte";
4
+ import {
5
+ Breadcrumb,
6
+ BreadcrumbList,
7
+ BreadcrumbItem,
8
+ BreadcrumbLink,
9
+ BreadcrumbPage,
10
+ BreadcrumbSeparator,
11
+ } from "$lib/components/ui/breadcrumb";
12
+
13
+ let { data, children }: { data: { user: { id: string; email: string } }; children: any } =
14
+ $props();
15
+
16
+ const segments = $derived(page.url.pathname.split("/").filter(Boolean));
17
+ const label = (s: string) => s[0].toUpperCase() + s.slice(1);
18
+ const hrefAt = (i: number) => "/" + segments.slice(0, i + 1).join("/");
19
+ </script>
20
+
21
+ <div class="flex min-h-screen">
22
+ <AdminSidebar currentPath={page.url.pathname} user={data.user} />
23
+ <main class="flex-1 overflow-x-hidden p-6">
24
+ {#if segments.length > 0}
25
+ <Breadcrumb class="mb-4">
26
+ <BreadcrumbList>
27
+ {#each segments as segment, i}
28
+ <BreadcrumbItem>
29
+ {#if i === segments.length - 1}
30
+ <BreadcrumbPage>{label(segment)}</BreadcrumbPage>
31
+ {:else}
32
+ <BreadcrumbLink href={hrefAt(i)}>{label(segment)}</BreadcrumbLink>
33
+ {/if}
34
+ </BreadcrumbItem>
35
+ {#if i < segments.length - 1}
36
+ <BreadcrumbSeparator />
37
+ {/if}
38
+ {/each}
39
+ </BreadcrumbList>
40
+ </Breadcrumb>
41
+ {/if}
42
+ {@render children()}
43
+ </main>
44
+ </div>
@@ -0,0 +1,11 @@
1
+ <!-- EDIT THIS FILE: add cards, KPIs, recent orders, sales charts, etc. -->
2
+ <svelte:head>
3
+ <title>Dashboard</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Dashboard</h1>
8
+ <p class="text-muted-foreground text-sm">
9
+ This is your admin home. Add widgets, KPI cards, and recent activity here.
10
+ </p>
11
+ </div>
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { page } from "bosia/client";
3
+ import PublicNavbar from "$lib/components/PublicNavbar.svelte";
4
+
5
+ let { data, children }: { data: { user: any }; children: any } = $props();
6
+ </script>
7
+
8
+ <div class="flex min-h-screen flex-col">
9
+ <PublicNavbar currentPath={page.url.pathname} user={data.user} />
10
+ <div class="flex-1">
11
+ {@render children()}
12
+ </div>
13
+ </div>
@@ -0,0 +1,38 @@
1
+ <svelte:head>
2
+ <title>Welcome to your shop</title>
3
+ <meta
4
+ name="description"
5
+ content="A Bosia shop starter — auth, RBAC, S3 uploads, products & cart."
6
+ />
7
+ </svelte:head>
8
+
9
+ <main class="flex min-h-[80vh] flex-col items-center justify-center gap-6 p-8">
10
+ <div class="flex flex-col items-center gap-3 text-center">
11
+ <img src="/favicon.svg" alt="" class="size-16" />
12
+ <h1 class="text-4xl font-bold tracking-tight">Welcome to your shop</h1>
13
+ <p class="text-muted-foreground text-lg">
14
+ A Bosia shop starter — auth, RBAC, S3 uploads, products & cart.
15
+ </p>
16
+ </div>
17
+
18
+ <div class="mt-4 flex gap-3">
19
+ <a
20
+ href="/products"
21
+ class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
22
+ >
23
+ Browse products
24
+ </a>
25
+ <a
26
+ href="/login"
27
+ class="border-border bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-md border px-4 py-2 text-sm font-medium transition-colors"
28
+ >
29
+ Sign in
30
+ </a>
31
+ </div>
32
+
33
+ <p class="text-muted-foreground mt-6 text-sm">
34
+ Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
35
+ >src/routes/(public)/+page.svelte</code
36
+ > to get started
37
+ </p>
38
+ </main>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { ErrorProps } from "./$types";
3
+ let { error }: ErrorProps = $props();
4
+ </script>
5
+
6
+ <svelte:head>
7
+ <title>{error.status} — {error.message}</title>
8
+ </svelte:head>
9
+
10
+ <div class="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
11
+ <p class="text-8xl font-bold text-gray-200">{error.status}</p>
12
+ <p class="text-2xl font-semibold text-gray-700">{error.message}</p>
13
+ <a
14
+ href="/"
15
+ class="mt-4 px-5 py-2 rounded-lg bg-gray-900 text-white text-sm hover:bg-gray-700 transition-colors"
16
+ >
17
+ Go home
18
+ </a>
19
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { LoadEvent } from "bosia";
2
+
3
+ export async function load({ locals }: LoadEvent) {
4
+ return {
5
+ appName: "Bosia Shop",
6
+ requestTime: (locals.requestTime as number | null) ?? null,
7
+ user: locals.user ?? null,
8
+ };
9
+ }
@@ -0,0 +1,6 @@
1
+ <script lang="ts">
2
+ import "../app.css";
3
+ let { children }: { children: any } = $props();
4
+ </script>
5
+
6
+ {@render children()}
@@ -0,0 +1,11 @@
1
+ {
2
+ "prebuilt": true,
3
+ "features": ["auth", "rbac", "file-upload", "shop"],
4
+ "featureOptions": {
5
+ "drizzle.dialect": "postgres",
6
+ "auth.dialect": "postgres",
7
+ "rbac.dialect": "postgres",
8
+ "file-upload.dialect": "postgres",
9
+ "shop.dialect": "postgres"
10
+ }
11
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "allowJs": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "noEmit": true,
11
+ "verbatimModuleSyntax": true,
12
+ "types": ["bun-types"],
13
+ "lib": ["dom", "dom.iterable", "esnext"],
14
+ "rootDirs": [".", ".bosia/types"],
15
+ "paths": {
16
+ "$lib": ["./src/lib"],
17
+ "$lib/*": ["./src/lib/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*", ".bosia/types/**/*.d.ts"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }