@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.
- package/AGENTS.md +45 -26
- package/README.md +118 -80
- package/index.js +4 -4
- package/package.json +10 -8
- package/template/.env.example +4 -8
- package/template/AGENTS.md +29 -28
- package/template/README.md +232 -398
- package/template/package.json +21 -12
- package/template/smrt.config.ts +1 -16
- package/template/src/app.d.ts +5 -4
- package/template/src/app.html +0 -1
- package/template/src/hooks.server.ts +27 -111
- package/template/src/lib/objects/Item.ts +23 -22
- package/template/src/lib/objects/index.ts +1 -1
- package/template/src/lib/server/smrt.ts +14 -22
- package/template/src/lib/server/tenancy.ts +81 -234
- package/template/src/routes/+layout.server.ts +12 -66
- package/template/src/routes/+layout.svelte +64 -76
- package/template/src/routes/+page.server.ts +67 -70
- package/template/src/routes/+page.svelte +141 -109
- package/template/src/routes/settings/+page.svelte +1 -1
- package/template/vite.config.ts +14 -4
- package/LICENSE +0 -7
package/template/README.md
CHANGED
|
@@ -1,467 +1,301 @@
|
|
|
1
|
-
#
|
|
1
|
+
# s-m-r-t SvelteKit starter
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
8
|
+
## 1. Install and run
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
89
|
-
</script>
|
|
49
|
+
## 3. Define the first object
|
|
90
50
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
re-runs, updating the page in place:
|
|
77
|
+
title: string = '';
|
|
78
|
+
description: string = '';
|
|
79
|
+
status: string = 'draft';
|
|
80
|
+
}
|
|
110
81
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
82
|
+
export class ItemCollection extends SmrtCollection<Item> {
|
|
83
|
+
static readonly _itemClass = Item;
|
|
84
|
+
}
|
|
114
85
|
|
|
115
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
138
|
-
model:
|
|
96
|
+
## 4. Initialize or migrate the database
|
|
139
97
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
export class Item extends SmrtObject { /* ... */ }
|
|
101
|
+
```bash
|
|
102
|
+
pnpm db:migrate
|
|
147
103
|
```
|
|
148
104
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
113
|
+
## 5. Understand tenant context
|
|
208
114
|
|
|
209
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
`
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
275
|
-
<!-- AdminShell + every page render themed inside here -->
|
|
276
|
-
</ThemeProvider>
|
|
277
|
-
```
|
|
135
|
+
## 6. Understand users, profiles, memberships, roles, and permissions
|
|
278
136
|
|
|
279
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
## 7. Load data into a SvelteKit page
|
|
354
174
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
201
|
+
## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
|
|
398
202
|
|
|
399
|
-
|
|
203
|
+
The Item configuration generates:
|
|
400
204
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
active: boolean = true;
|
|
419
|
-
}
|
|
214
|
+
```bash
|
|
215
|
+
pnpm smrt objects
|
|
216
|
+
pnpm smrt schema Item
|
|
420
217
|
```
|
|
421
218
|
|
|
422
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
230
|
+
The output is `.smrt/mcp-server/index.js`. It is generated and ignored.
|
|
430
231
|
|
|
431
|
-
|
|
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
|
-
|
|
435
|
-
|
|
236
|
+
pnpm add @happyvertical/smrt-web@0.38.25
|
|
237
|
+
```
|
|
436
238
|
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
smrt generate-routes
|
|
262
|
+
## 9. Add optional live browser data
|
|
445
263
|
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
##
|
|
295
|
+
## 10. Graduate to smrt-saas-starter
|
|
465
296
|
|
|
466
|
-
|
|
467
|
-
-
|
|
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.
|