@happyvertical/smrt-template-sveltekit 0.38.26 → 0.39.0

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.
@@ -1,467 +1,301 @@
1
- # SMRT SvelteKit App
1
+ # s-m-r-t SvelteKit starter
2
2
 
3
- A SvelteKit application with SMRT framework integration for rapid development of AI-powered, multi-tenant applications.
3
+ This is the small, ground-up starting point for s-m-r-t 0.38.25. It contains
4
+ one object and the current SvelteKit application foundation. It intentionally
5
+ does not include billing, onboarding, workers, deployment infrastructure, or
6
+ provider-specific production configuration.
4
7
 
5
- ## Getting Started
8
+ ## 1. Install and run
6
9
 
7
- 1. **Install dependencies**:
8
- ```bash
9
- npm install
10
- ```
10
+ Requirements: Node.js 24.18.0 or newer and pnpm 10.34.4. The exact pnpm version
11
+ is declared in `packageManager`.
11
12
 
12
- 2. **Set up environment**:
13
- ```bash
14
- cp .env.example .env
15
- ```
16
-
17
- 3. **Start development server**:
18
- ```bash
19
- npm run dev
20
- ```
21
-
22
- 4. **Initialize database** (optional):
23
- ```bash
24
- smrt db:setup
25
- ```
26
-
27
- ## Project Structure
28
-
29
- ```
30
- ├── src/
31
- │ ├── hooks.server.ts # Auth + tenancy wiring (see "Multi-tenancy" below)
32
- │ ├── lib/
33
- │ │ ├── objects/ # SMRT objects (auto-generates API routes)
34
- │ │ │ ├── index.ts # Export all objects here
35
- │ │ │ └── Item.ts # Example SMRT object
36
- │ │ └── server/
37
- │ │ ├── smrt.ts # SMRT configuration + local manifest seeding
38
- │ │ ├── smrt-register.ts # Object registration (generated by smrtPlugin, gitignored)
39
- │ │ └── tenancy.ts # Pluggable tenant resolver
40
- │ └── routes/
41
- │ ├── api/ # Auto-generated API routes (don't edit!)
42
- │ ├── +layout.server.ts # Builds AdminShell tenant nav server-side (manifest → nav)
43
- │ ├── +layout.svelte # WASD AdminShell chrome wrapping every page
44
- │ ├── settings/ # ShellSettingsPanel (panel layout + hotkeys)
45
- │ ├── +page.server.ts # Home page server load + demo mutation (reference pattern)
46
- │ └── +page.svelte # Home page (renders inside AdminShell)
47
- ├── smrt.config.ts # Root SMRT config
48
- ├── vite.config.ts # Vite + SMRT plugin config
49
- └── svelte.config.js # SvelteKit config
13
+ ```bash
14
+ pnpm install
15
+ cp .env.example .env
16
+ pnpm db:migrate
17
+ pnpm check
18
+ pnpm build
19
+ pnpm dev
50
20
  ```
51
21
 
52
- ## Data loading (SSR + hydration)
22
+ Open `http://localhost:5173`. The app loads, but tenant data remains closed
23
+ until your application adds a sign-in flow, an active membership, and role
24
+ permissions. That is intentional: the starter demonstrates safe boundaries
25
+ without inventing an authentication provider.
53
26
 
54
- The home page (`src/routes/+page.server.ts` + `+page.svelte`) is the reference
55
- pattern for getting collection data onto a page. Copy it for your own routes.
27
+ `pnpm db:migrate` first runs the Vite build so the manifest, runtime
28
+ registration, generated routes, and types match the current objects; it then
29
+ applies the manifest-derived schema to SQLite. Re-run it after object changes.
56
30
 
57
- **Query collections in a server `load`, not with `fetch` on mount.** A server
58
- load runs during SSR, reads the database directly through the collection API
59
- (no HTTP round-trip through your own `/api/*` routes), and SvelteKit
60
- serializes the result into the initial HTML. The client hydrates that data
61
- without re-fetching — the page renders with its data on first paint, and the
62
- first client render issues no duplicate request.
31
+ ## 2. Understand the generated files
63
32
 
64
- ```typescript
65
- // src/routes/+page.server.ts
66
- import type { Item } from '$lib/objects/Item';
67
- import { getCollection } from '$lib/server/smrt';
68
- import type { PageServerLoad } from './$types';
33
+ The source of truth is `src/lib/objects`. Running `pnpm dev`, `pnpm build`, or
34
+ `pnpm db:migrate` regenerates these artifacts:
69
35
 
70
- export const load: PageServerLoad = async ({ depends }) => {
71
- depends('smrt:items'); // see "Refreshing after mutations" below
72
-
73
- const items = await getCollection<Item>('Item');
74
- const rows = await items.list({ orderBy: 'created_at DESC', limit: 50 });
36
+ | Path | Purpose | Commit it? |
37
+ | --- | --- | --- |
38
+ | `.smrt/manifest.json` | Merged local and dependency runtime manifest | No |
39
+ | `.smrt/smrt-knowledge.json` | Agent/developer knowledge graph | No |
40
+ | `.smrt/register.js` | External package registration used by the CLI | No |
41
+ | `src/lib/server/smrt-register.ts` | Local runtime class registration | No |
42
+ | `src/lib/types/smrt-generated/` | Virtual-module and consumer declarations | No |
43
+ | `src/routes/api/**/+server.ts` | Generated SvelteKit REST routes | No |
75
44
 
76
- // Return plain serializable objects, not class instances.
77
- return {
78
- items: rows.map((item) => ({ id: item.id, title: item.title, status: item.status })),
79
- };
80
- };
81
- ```
82
-
83
- ```svelte
84
- <!-- src/routes/+page.svelte -->
85
- <script lang="ts">
86
- import type { PageProps } from './$types';
45
+ Do not edit generated files. `smrtPlugin()` owns local scanning, manifests,
46
+ types, and routes. `smrtConsumer()` explicitly consumes the profiles, tenancy,
47
+ and users manifests so those models are available to setup and tooling.
87
48
 
88
- let { data }: PageProps = $props();
89
- </script>
49
+ ## 3. Define the first object
90
50
 
91
- {#each data.items as item (item.id)}
92
- <p>{item.title}</p>
93
- {/each}
94
- ```
51
+ `src/lib/objects/Item.ts` is the only example. It exposes the same CRUD action
52
+ set to REST, MCP, WebMCP definitions, and the CLI, while limiting writable REST
53
+ fields and opting into tenant scoping:
95
54
 
96
- Avoid the mount-time fetch anti-pattern (`$effect(() => { fetch('/api/items') ... })`):
97
- it renders an empty page first, waterfalls a second request on every
98
- navigation, and skips SvelteKit's load/hydration machinery entirely. Reserve
99
- client-side `fetch` of the generated `/api/*` routes for after-load
100
- interactions (mutations, polling, external clients) — not for initial page
101
- data.
55
+ ```ts
56
+ import {
57
+ ObjectRegistry,
58
+ SmrtCollection,
59
+ SmrtObject,
60
+ smrt,
61
+ } from '@happyvertical/smrt-core';
62
+ import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
102
63
 
103
- ### Refreshing after mutations (`depends` / `invalidate`)
64
+ @smrt({
65
+ api: {
66
+ include: ['list', 'get', 'create', 'update', 'delete'],
67
+ writable: ['title', 'description', 'status'],
68
+ },
69
+ cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
70
+ mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
71
+ })
72
+ @TenantScoped({ mode: 'optional' })
73
+ export class Item extends SmrtObject {
74
+ @tenantId({ nullable: true })
75
+ tenantId: string | null = null;
104
76
 
105
- Convention: each load declares one dependency key per collection it reads,
106
- named `smrt:<collection>` — where `<collection>` is the generated REST route
107
- segment (`/api/items` → `smrt:items`). After a mutation, client code calls
108
- `invalidate('smrt:<collection>')` and every load that declared that key
109
- re-runs, updating the page in place:
77
+ title: string = '';
78
+ description: string = '';
79
+ status: string = 'draft';
80
+ }
110
81
 
111
- ```svelte
112
- <script lang="ts">
113
- import { invalidate } from '$app/navigation';
82
+ export class ItemCollection extends SmrtCollection<Item> {
83
+ static readonly _itemClass = Item;
84
+ }
114
85
 
115
- async function createItem(title: string) {
116
- await fetch('/api/items', {
117
- method: 'POST',
118
- headers: { 'content-type': 'application/json' },
119
- body: JSON.stringify({ title }),
120
- });
121
- await invalidate('smrt:items'); // re-runs every load that declared depends('smrt:items')
122
- }
123
- </script>
86
+ ObjectRegistry.registerCollection('Item', ItemCollection);
124
87
  ```
125
88
 
126
- The home page demonstrates the same flow with a progressively-enhanced form
127
- action: `use:enhance` applies the action result with
128
- `update({ invalidateAll: false })`, then calls `invalidate('smrt:items')` so
129
- only the loads that depend on items re-run (instead of SvelteKit's default
130
- invalidate-everything). Note that the generated `/api/*` routes are
131
- fail-closed — mutations 401 unless the request is authenticated or the object
132
- opts in via `@smrt({ api: { public: true } })` — which is why the demo uses a
133
- form action running server-side.
134
-
135
- ### Caching SSR reads
89
+ Add another class beside it and export the class from
90
+ `src/lib/objects/index.ts`. Keep relationship decorators next to `@smrt()`.
91
+ Use `@foreignKey(Target)` for same-package relationships and
92
+ `@crossPackageRef()` for relationships to another package. Keep the explicit
93
+ collection constructor/registration when the generated CLI or MCP runtime must
94
+ construct the collection outside a SvelteKit request.
136
95
 
137
- Server-side reads can opt into the collection read cache, per call or per
138
- model:
96
+ ## 4. Initialize or migrate the database
139
97
 
140
- ```typescript
141
- // Per call — this load only
142
- const rows = await items.list({ where: { status: 'published' }, cache: { ttl: 60_000 } });
98
+ SQLite defaults to `./app.db`; override it with `DATABASE_URL` and
99
+ `DATABASE_TYPE`.
143
100
 
144
- // Per model — every read of this collection
145
- @smrt({ cache: { ttl: 60_000 } })
146
- export class Item extends SmrtObject { /* ... */ }
101
+ ```bash
102
+ pnpm db:migrate
147
103
  ```
148
104
 
149
- Cached rows are keyed by the final SQL + parameters and live for `ttl`
150
- milliseconds. Every mutation that goes through SMRT (`create()`, `save()`,
151
- `delete()`) invalidates the table's cached entries in this process
152
- automatically — so the post-mutation `invalidate('smrt:...')` re-run sees
153
- fresh rows, as the home page demonstrates.
154
-
155
- Use it for read-heavy, write-rare data rendered for many visitors: catalogs,
156
- published content, navigation. Do **not** cache:
157
-
158
- - **Per-user data** (dashboards, "my items"): each user's query shape churns
159
- the cache for little reuse, and any staleness is personally visible.
160
- - **Admin editors / read-your-writes flows**: editors must always see the
161
- row they just changed, including across processes.
162
- - **Multi-replica deployments without `crossProcess: true`**: the cache is
163
- per-process, so a write on one replica leaves the others stale until TTL.
164
- Set `cache: { ttl, crossProcess: true }` (uses the database adapter's
165
- notification capability, e.g. Postgres LISTEN/NOTIFY) or keep the TTL
166
- short enough to tolerate.
167
-
168
- Pass `cache: false` on a specific call to bypass a model-level cache where
169
- freshness matters.
170
-
171
- ### Client live collections (opt-in)
172
-
173
- The server-load pattern above renders a page and refreshes it with
174
- `invalidate()`. For **interactive surfaces** that mutate rows and want an
175
- optimistic, reactive local store (an editor, a live dashboard), layer the
176
- browser client-data runtime on top: `@happyvertical/smrt-web` (the engine
177
- wrapper) + `@happyvertical/smrt-svelte/web` (the Svelte 5 runes binding). It is
178
- **opt-in per surface** — the client engine (~76 kB gzip) should never load on
179
- public or content pages, so import it only in routes that use live collections.
180
-
181
- Keep loading the initial rows in a server `load` (SSR + no waterfall), then
182
- **seed** the client collection from the hydrated `PageData` with `initialData`,
183
- so the first client render serves the SSR rows with no duplicate fetch and only
184
- revalidates once they go stale:
185
-
186
- ```typescript
187
- // src/routes/live/+page.server.ts — same server-load pattern, plain rows out.
188
- import { getCollection } from '$lib/server/smrt';
189
- import type { Item } from '$lib/objects/Item';
190
- import type { PageServerLoad } from './$types';
191
-
192
- export const load: PageServerLoad = async () => {
193
- const items = await getCollection<Item>('Item');
194
- const rows = await items.list({ orderBy: 'created_at DESC', limit: 50 });
195
- return { items: rows.map((i) => ({ id: i.id, title: i.title, status: i.status })) };
196
- };
197
- ```
105
+ The current command is `smrt db:migrate`. `smrt db:setup` is deprecated in
106
+ 0.38.25 and is intentionally not used. Migrations are manifest-driven: change
107
+ the TypeScript object, regenerate the manifest, and run the migration again.
108
+ There are no hand-written migration files in this workflow.
198
109
 
199
- ```svelte
200
- <!-- src/routes/live/+page.svelte -->
201
- <script lang="ts">
202
- import { createSmrtCollection } from '@happyvertical/smrt-web';
203
- import { liveCollection } from '@happyvertical/smrt-svelte/web';
204
- import { getCollectionDefinition } from '@happyvertical/smrt-virt-web';
205
- import type { PageProps } from './$types';
110
+ Runtime schema creation is disabled. A missing table should be fixed by the
111
+ migration command, not by adding schema creation to a request handler.
206
112
 
207
- let { data }: PageProps = $props();
113
+ ## 5. Understand tenant context
208
114
 
209
- // initialData seeds the cache from the SSR rows: the first read is served
210
- // from data.items with NO network request, and revalidates after staleTimeMs.
211
- // basePath MUST match this app's generated routes — they are served at
212
- // `/api/*` (routesDir: 'src/routes/api'), not the runtime's `/api/v1` default,
213
- // so revalidation and live mutations hit `/api/items` instead of 404ing.
214
- const items = createSmrtCollection(getCollectionDefinition('items'), {
215
- initialData: data.items,
216
- basePath: '/api',
217
- });
218
- const view = liveCollection(items);
219
- </script>
115
+ `src/hooks.server.ts` keeps tenant selection separate from authorization:
220
116
 
221
- {#each view.rows as item (item.id)}
222
- <p>{item.title}</p>
223
- {/each}
224
- ```
117
+ 1. `src/lib/server/tenancy.ts` reads a subdomain slug and looks up an active
118
+ Tenant UUID. It stores the candidate in `locals.selectedTenantId` and
119
+ `locals.selectedTenantSlug`.
120
+ 2. That candidate does not enter AsyncLocalStorage and cannot scope queries.
121
+ 3. `createSessionHandler({ enterTenantContext: true })` loads the signed session,
122
+ resolves its membership and permissions, and establishes the authorized
123
+ `locals.tenantId` context.
124
+ 4. `enableTenancy()` makes `@TenantScoped` collections honor that context.
225
125
 
226
- `getCollectionDefinition('items')` comes from the plugin-generated
227
- `@happyvertical/smrt-virt-web` virtual module (its `<collection>` key is the
228
- same REST route segment as `depends`/`invalidate`). Add
229
- `@happyvertical/smrt-web` and `@happyvertical/smrt-svelte` to the project's
230
- dependencies before using this pattern.
231
-
232
- ## Workspace shell (AdminShell)
233
-
234
- This template renders every page inside the **WASD AdminShell** — a four-edge
235
- admin layout from `@happyvertical/smrt-svelte/workspace`. Each edge is a scope
236
- (top = app, left = tenant, right = focus, bottom = system) toggled with
237
- <kbd>W</kbd>/<kbd>A</kbd>/<kbd>S</kbd>/<kbd>D</kbd> (press <kbd>?</kbd> for the
238
- shortcuts). It is wired in the root layout:
239
-
240
- - `src/routes/+layout.server.ts` builds the left "tenant" nav **server-side**
241
- with `tenantNavFromManifest()` from the SMRT manifest (`.smrt/manifest.json`)
242
- and returns it as layout data — no client-side nav fetch.
243
- - `src/routes/+layout.svelte` wraps `{@render children()}` in `<AdminShell>`
244
- (inside `<ThemeProvider>` — see **Theming** below), feeding that nav to
245
- `<TenantNav>`, a brand/app panel on top, and a couple of status chips on the
246
- bottom.
247
- - `src/routes/settings/+page.svelte` drops in `<ShellSettingsPanel>` so users
248
- can toggle panels and remap the hotkeys; the choices persist in
249
- `localStorage` under the shell's `storageKey`.
250
-
251
- **The shell preserves the server-load pattern above.** AdminShell's public core
252
- is SSR-safe: it renders statically on the server and only activates hotkeys /
253
- `localStorage` after mount. The active page still renders inside the shell's
254
- `<main>` and keeps its own `+page.server.ts` load and `invalidate()` refresh —
255
- the shell is chrome around the page, not a replacement for its data flow. The
256
- home page's `depends('smrt:items')` / `invalidate('smrt:items')` cycle works
257
- unchanged.
258
-
259
- ### Theming
260
-
261
- The layout wraps the shell in **`<ThemeProvider>`** from
262
- `@happyvertical/smrt-ui/themes` — the standard SMRT theming wrapper (the same
263
- pattern the reference `@happyvertical/smrt-content` app uses):
126
+ The default resolver ignores `x-tenant-id`. If a gateway supplies a tenant
127
+ header, validate the gateway identity/signature before mapping it to a tenant,
128
+ and still use `switchSessionTenant()` for browser session changes. That helper
129
+ checks active membership and rotates the session ID; never copy an untrusted
130
+ header directly into `locals.tenantId` or `enterTenantContext()`.
264
131
 
265
- ```svelte
266
- <script lang="ts">
267
- import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
268
- // @font-face rules for the SMRT type stack (Space Grotesk / Inter /
269
- // JetBrains Mono woff2). ThemeProvider supplies the token *variables*;
270
- // this loads the font *files*. Bundled — no CDN request.
271
- import '@happyvertical/smrt-ui/themes/styles/fonts.css';
272
- </script>
132
+ Set `TENANT_BASE_DOMAIN` for deployed subdomain routing. The fallback parser is
133
+ only for local shapes such as `acme.demo.local`.
273
134
 
274
- <ThemeProvider colorScheme="system" persist={true}>
275
- <!-- AdminShell + every page render themed inside here -->
276
- </ThemeProvider>
277
- ```
135
+ ## 6. Understand users, profiles, memberships, roles, and permissions
278
136
 
279
- - ThemeProvider injects the **entire `--smrt-*` token set** (colors, typography,
280
- spacing, radius, elevation, motion, z-index) as an inline style it computes
281
- during render — so the tokens are present in the SSR HTML with **no unstyled
282
- flash**, and it re-resolves them on the client. You do **not** need to import
283
- `styles/tokens.css` or `themes/styles/*.css` separately; the provider is the
284
- runtime source of truth.
285
- - `colorScheme="system"` follows the OS light/dark preference; `persist` saves a
286
- user's explicit switch to `localStorage`. Switch presets/scheme at runtime
287
- with `ThemeSwitcher` / `ColorSchemeToggle` (also from `.../themes`), or read
288
- the current theme via the theme context.
289
- - The one thing the provider does not carry is the font **files** — hence the
290
- `themes/styles/fonts.css` import above. Drop it and the type stack degrades to
291
- `system-ui` / `ui-monospace` automatically.
292
-
293
- To pin a specific look instead of following the OS, set
294
- `colorScheme="light"` (or `"dark"`) and `preset="material"` (or `"glass"` /
295
- `"studio"` / `"smrt"`).
296
-
297
- The status chips ship static (`Local`, `Ready`). To make them (and the focus
298
- edge) live — job counts, dispatch depth, connection state — feed real values
299
- through `systemFeed` (`@happyvertical/smrt-svelte/workspace/live`) or
300
- `activityFeed` (`@happyvertical/smrt-svelte/web`).
301
-
302
- - **Migration guide** (old `WorkspaceShell`/`RoleShell` → `AdminShell`, and why
303
- adoption is additive):
304
- [`@happyvertical/smrt-svelte` → `src/components/workspace/MIGRATION.md`](https://github.com/happyvertical/smrt/blob/main/packages/smrt-svelte/src/components/workspace/MIGRATION.md)
305
- - **Four-scope demo** (all edges, focus tools, activities) and the live-feed
306
- variants, in the smrt-svelte playground:
307
- `playground/src/routes/admin-shell`, `admin-shell-activity-feed`, and
308
- `admin-shell-system-feed`.
309
-
310
- ## Multi-tenancy
311
-
312
- This template ships with multi-tenancy pre-wired. Out of the box you get:
313
-
314
- - **Session loading + auth** via `createSessionHandler({ enterTenantContext: true })` from `@happyvertical/smrt-users/sveltekit`. After the hook runs, `event.locals` carries `{ user, permissions, tenantId, sessionId }`.
315
- - **Auto-scoped REST routes**. The tenancy interceptor is registered globally (`enableTenancy()` in `hooks.server.ts`), so any model decorated with `@TenantScoped()` is filtered by the current tenant in `AsyncLocalStorage`. The generated `src/routes/api/**` endpoints inherit this automatically.
316
- - **Subdomain-based tenant resolution**. By default, the leading subdomain is the tenant slug:
317
-
318
- | URL | Tenant ID |
319
- |---|---|
320
- | `https://acme.demo.local/dashboard` | `acme` |
321
- | `https://www.demo.local/` | `null` (reserved) |
322
- | `https://demo.local/` | `null` (no subdomain) |
323
- | `http://localhost:5173/` | `null` (root-like host) |
324
-
325
- ### Local development DNS
326
-
327
- Browsers won't resolve subdomains of `demo.local` to your dev server automatically. Pick one:
328
-
329
- **Option A — `/etc/hosts` (simplest, fixed list)**
137
+ These are separate records with separate responsibilities:
330
138
 
331
- ```
332
- 127.0.0.1 demo.local
333
- 127.0.0.1 acme.demo.local
334
- 127.0.0.1 shop.acme.demo.local
335
- ```
139
+ - User is the authentication identity.
140
+ - Profile is person-facing identity and metadata; a User may reference one.
141
+ - Tenant is an organization/security boundary.
142
+ - Membership connects one User to one Tenant and one Role.
143
+ - Role receives Permission records through RolePermission.
144
+ - Session binds the authenticated user to an active tenant and publishes the
145
+ resolved permission set for the request.
336
146
 
337
- **Option B — `dnsmasq` (wildcard, recommended)**
147
+ CRUD permissions are manifest-derived: `items.read`, `items.create`,
148
+ `items.update`, and `items.delete`. Provisioning code should sync the catalog
149
+ and seed roles after the database migration:
338
150
 
339
- ```bash
340
- # macOS
341
- brew install dnsmasq
342
- echo 'address=/demo.local/127.0.0.1' | sudo tee -a $(brew --prefix)/etc/dnsmasq.conf
343
- sudo brew services restart dnsmasq
344
- # Tell macOS to use dnsmasq for `.local` queries
345
- sudo mkdir -p /etc/resolver
346
- echo 'nameserver 127.0.0.1' | sudo tee /etc/resolver/local
151
+ ```ts
152
+ import {
153
+ RoleCollection,
154
+ syncPermissionCatalog,
155
+ } from '@happyvertical/smrt-users';
156
+ import { getSmrtConfig } from '$lib/server/smrt';
157
+
158
+ await syncPermissionCatalog(getSmrtConfig('Permission'));
159
+ const roles = await RoleCollection.create(getSmrtConfig('Role'));
160
+ await roles.seedSystemRoles({ seedPermissions: true });
347
161
  ```
348
162
 
349
- After either, hit `http://acme.demo.local:5173/` and the tenant will resolve to `acme`.
163
+ The home-page form action demonstrates the hand-written server boundary: it
164
+ passes the session's exact `locals.permissions` snapshot to
165
+ `assertOperationPermission()`. Keep that pattern for custom SvelteKit actions,
166
+ endpoints, jobs running as a principal, and other in-process writes.
350
167
 
351
- ### Swapping the resolution strategy
168
+ Generated REST routes are authentication-gated and tenant-scoped. On SQLite,
169
+ authentication is not a substitute for your app's operation policy. Put
170
+ permission-checked mutations behind app-owned handlers, or use the framework's
171
+ Postgres RLS setup when moving to a production database.
352
172
 
353
- `src/lib/server/tenancy.ts` exposes three built-in strategies plus a `createTenantResolver()` factory. Pick whichever matches your routing:
173
+ ## 7. Load data into a SvelteKit page
354
174
 
355
- ```ts
356
- // Subdomain (default)
357
- import {
358
- createTenantResolver,
359
- subdomainStrategy,
360
- } from '$lib/server/tenancy';
361
- export const resolveTenant = createTenantResolver(subdomainStrategy);
175
+ Initial data belongs in `+page.server.ts`, where it can use the database and
176
+ session context directly. Return plain serializable rows:
362
177
 
363
- // Path prefix: /t/<slug>/...
364
- import {
365
- createTenantResolver,
366
- pathPrefixStrategy,
367
- } from '$lib/server/tenancy';
368
- export const resolveTenant = createTenantResolver(pathPrefixStrategy());
369
-
370
- // Custom prefix: /tenant/<slug>/...
371
- export const resolveTenant = createTenantResolver(
372
- pathPrefixStrategy({ prefix: '/tenant/' }),
373
- );
178
+ ```ts
179
+ export const load: PageServerLoad = async ({ depends, locals }) => {
180
+ depends('smrt:items');
374
181
 
375
- // HTTP header: x-tenant-id: acme
376
- import {
377
- createTenantResolver,
378
- headerStrategy,
379
- } from '$lib/server/tenancy';
380
- export const resolveTenant = createTenantResolver(headerStrategy());
182
+ if (!locals.permissions.includes('items.read')) {
183
+ return { items: [] };
184
+ }
381
185
 
382
- // Compose: try subdomain, fall back to header
383
- import {
384
- createTenantResolver,
385
- headerStrategy,
386
- subdomainStrategy,
387
- } from '$lib/server/tenancy';
388
- export const resolveTenant = createTenantResolver((event) => {
389
- const fromSubdomain = subdomainStrategy(event);
390
- if (fromSubdomain.tenantId) return fromSubdomain;
391
- return headerStrategy()(event);
392
- });
186
+ const items = await getCollection<Item>('Item');
187
+ const rows = await items.list({ limit: 50 });
188
+ return {
189
+ items: rows.flatMap((item) =>
190
+ item.id ? [{ id: item.id, title: item.title, status: item.status }] : [],
191
+ ),
192
+ };
193
+ };
393
194
  ```
394
195
 
395
- Inside a `+server.ts` or `+page.server.ts` you can read the resolved tenant either from `event.locals.tenantId` (set by the session handler) or — for any tenant-scoped model — just call its collection methods and the global interceptor will filter automatically.
196
+ The page receives that data through `$props()`. Do not fetch initial page data
197
+ from `onMount` or `$effect`; SvelteKit already serialized it into the response.
198
+ After a mutation, call `invalidate('smrt:items')`. Only loads that declared
199
+ `depends('smrt:items')` re-run.
396
200
 
397
- ## Creating SMRT Objects
201
+ ## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
398
202
 
399
- 1. Create a new file in `src/lib/objects/`:
203
+ The Item configuration generates:
400
204
 
401
- ```typescript
402
- // src/lib/objects/Product.ts
403
- import { SmrtObject, smrt } from '@happyvertical/smrt-core';
404
- import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
205
+ - REST: `GET`/`POST /api/items` and
206
+ `GET`/`PUT`/`DELETE /api/items/[id]`.
207
+ - MCP descriptors and tools for Item CRUD.
208
+ - Web collection definitions in the virtual
209
+ `@happyvertical/smrt-virt-web` module.
210
+ - CLI commands for Item CRUD.
405
211
 
406
- @smrt({
407
- api: { include: ['list', 'get', 'create', 'update', 'delete'] },
408
- cli: { include: ['list', 'get'] },
409
- })
410
- @TenantScoped({ mode: 'optional' })
411
- export class Product extends SmrtObject {
412
- @tenantId({ nullable: true })
413
- tenantId: string | null = null;
212
+ Inspect the registered objects and use the example CLI:
414
213
 
415
- name: string = '';
416
- price: number = 0.0;
417
- description: string = '';
418
- active: boolean = true;
419
- }
214
+ ```bash
215
+ pnpm smrt objects
216
+ pnpm smrt schema Item
420
217
  ```
421
218
 
422
- 2. Export it from `src/lib/objects/index.ts`:
219
+ The 0.38.25 CLI's manifest-only `objects` and `schema` commands work directly
220
+ in this source-first template. Executing local-object CRUD through the generic
221
+ CLI additionally requires a compiled JavaScript project entry point; REST and
222
+ the page action are the runnable CRUD examples here.
423
223
 
424
- ```typescript
425
- export { Item } from './Item.js';
426
- export { Product } from './Product.js';
224
+ Generate a standalone MCP server when you are ready to configure a transport:
225
+
226
+ ```bash
227
+ pnpm smrt generate-mcp --no-config --no-readme
427
228
  ```
428
229
 
429
- 3. Run `npm run dev` - API routes are auto-generated! Because `Product` is `@TenantScoped`, listing via `GET /api/products` will only return rows for the request's tenant. (The generator pluralizes each class's `collection` field; see the "Generated API Routes" table below for the general `/api/{collection}` form.)
230
+ The output is `.smrt/mcp-server/index.js`. It is generated and ignored.
430
231
 
431
- ## CLI Commands
232
+ WebMCP is opt-in per browser surface. First add the browser runtime, then put
233
+ registration in a dedicated page that actually exposes tools:
432
234
 
433
235
  ```bash
434
- # List discovered SMRT objects
435
- smrt objects
236
+ pnpm add @happyvertical/smrt-web@0.38.25
237
+ ```
436
238
 
437
- # View object details
438
- smrt introspect
239
+ ```svelte
240
+ <script lang="ts">
241
+ import { collectionDefinitions } from '@happyvertical/smrt-virt-web';
242
+ import { onMount } from 'svelte';
243
+
244
+ onMount(() => {
245
+ let dispose = () => {};
246
+ void import('@happyvertical/smrt-web').then(({ registerWebMcpTools }) => {
247
+ dispose = registerWebMcpTools([collectionDefinitions.items], {
248
+ basePath: '/api',
249
+ });
250
+ });
251
+ return () => dispose();
252
+ });
253
+ </script>
254
+ ```
439
255
 
440
- # Initialize database tables
441
- smrt db:setup
256
+ `registerWebMcpTools()` feature-detects browser support and uses the current
257
+ authenticated page session. In 0.38.25, list/get/create/update/delete are wired.
258
+ Generated descriptors for custom actions exist, but custom action execution is
259
+ not yet wired; WebMCP returns a clear result for those actions. Filter to read-only on
260
+ surfaces that should not offer browser mutations.
442
261
 
443
- # Regenerate API routes
444
- smrt generate-routes
262
+ ## 9. Add optional live browser data
445
263
 
446
- # Run operations on objects
447
- smrt item list
448
- smrt item get <id>
449
- ```
264
+ Use this only on an interactive page. `@happyvertical/smrt-web` must be a direct
265
+ dependency because the page imports it; the base starter does not need it.
450
266
 
451
- ## API Endpoints
267
+ Keep the server load from section 7, then seed the browser collection from its
268
+ hydrated rows so the first render does not issue a duplicate request:
452
269
 
453
- For each SMRT object, the following endpoints are auto-generated:
270
+ ```svelte
271
+ <script lang="ts">
272
+ import { createSmrtCollection } from '@happyvertical/smrt-web';
273
+ import { liveCollection } from '@happyvertical/smrt-svelte/web';
274
+ import { getCollectionDefinition } from '@happyvertical/smrt-virt-web';
275
+ import type { PageProps } from './$types';
276
+
277
+ let { data }: PageProps = $props();
278
+
279
+ const items = createSmrtCollection(getCollectionDefinition('items'), {
280
+ basePath: '/api',
281
+ initialData: data.items,
282
+ staleTimeMs: 30_000,
283
+ });
284
+ const view = liveCollection(items);
285
+ </script>
454
286
 
455
- - `GET /api/{collection}` - List all items
456
- - `GET /api/{collection}/[id]` - Get single item
457
- - `POST /api/{collection}` - Create new item
458
- - `PUT /api/{collection}/[id]` - Update item
459
- - `DELETE /api/{collection}/[id]` - Delete item
287
+ {#each view.rows as item (item.id)}
288
+ <p>{item.title}</p>
289
+ {/each}
290
+ ```
460
291
 
461
- Custom methods are exposed as:
462
- - `POST /api/{collection}/[id]/{method}` - Call custom method
292
+ Import the live runtime only from routes that use it. Static pages and the root
293
+ layout should keep using server loads and should not pay for browser data tools.
463
294
 
464
- ## Learn More
295
+ ## 10. Graduate to smrt-saas-starter
465
296
 
466
- - [SMRT Framework Documentation](https://github.com/happyvertical/smrt)
467
- - [SvelteKit Documentation](https://kit.svelte.dev/)
297
+ Stay here while you are learning the object model or building a focused app
298
+ from first principles. Move to `smrt-saas-starter` when you want a
299
+ production-shaped SaaS baseline with onboarding, billing/subscriptions,
300
+ background workers, provider configuration, deployment conventions, and
301
+ mobile surfaces. Those concerns are intentionally absent here.