@happyvertical/smrt-template-sveltekit 0.55.16 → 0.55.18

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 CHANGED
@@ -84,17 +84,34 @@ It is the ground-up alternative to `smrt-saas-starter`.
84
84
  establishes tenant context. Never turn an untrusted header into authority.
85
85
  - App code uses `runtime.getCollection()`/`runtime.classOptions()`, which are
86
86
  request-scoped (RLS transaction) and must never be retained.
87
- - `Item` is the single example object and demonstrates optional tenant scope,
88
- a REST writable allowlist, shared CRUD action metadata, and the explicit
87
+ - `Note` is the single example object (the canonical Notes starter, matching
88
+ `smrt-start`) and demonstrates required tenant scope, a REST writable allowlist, shared CRUD action metadata, and the explicit
89
89
  collection registration required by generated CLI/MCP runtime commands.
90
90
  - Initial data comes from `+page.server.ts`; loads declare
91
- `depends('smrt:items')`, mutations call `invalidate('smrt:items')`, and
91
+ `depends('smrt:notes')`, mutations call `invalidate('smrt:notes')`, and
92
92
  hand-written writes call `assertOperationPermission()` with the session's
93
93
  exact permission snapshot.
94
94
  - The root layout is `AppShell` from `@happyvertical/smrt-svelte/app` with a
95
95
  small explicit `nav`; `/setup` and `/settings` mount that package's
96
96
  `OwnerSetupForm` and `ShellSettingsPage`. Generated REST routes do not imply
97
97
  page routes.
98
+ - Session user and sign-out: `+layout.server.ts` (`createSessionLayoutLoad()`)
99
+ returns `session.user` (`{ id, email, name? }` only, never the record or
100
+ tokens). The root layout passes it as AppShell `user` (account menu) with
101
+ `onSignOut` from `src/lib/sign-out.ts` (same-origin `fetch` `POST /sign-out`,
102
+ then `invalidateAll()` and `goto('/')`); the settings page passes the same
103
+ helper. `src/routes/sign-out/+server.ts` mounts `createSignOutHandler(runtime)`
104
+ (POST only, 405 otherwise; same-origin only, 403 otherwise; 204, or 303 to `/`
105
+ for an HTML form post). The layout enables `phoneTabBar` and sets
106
+ `chrome={!page.route.id?.startsWith('/setup')}` so `/setup` renders only the
107
+ chrome-free `OwnerSetupForm` card.
108
+ - The Notes page is built from smrt-ui primitives only (`Button`, `Card`,
109
+ `Form`, `FormGroup`, `Input`, `Textarea`, `EmptyState`): no raw `<form>`,
110
+ `<input>`, or `<button>`, token spacing, and a "Start your first note" empty
111
+ state. The root layout passes only live AppShell props (no `subtitle`,
112
+ `tenantLabel`, or `appPanelDocs`) and gives each nav item a shell icon name.
113
+ `app.html` carries `viewport-fit=cover`, `color-scheme`, light and dark
114
+ `theme-color`, and `static/favicon.svg`.
98
115
  - AppShell's Provider registers generated WebMCP read tools with the
99
116
  authenticated page session. Write/destructive effects require an explicit
100
117
  page-owned policy.
package/README.md CHANGED
@@ -70,13 +70,13 @@ lifecycle script is `smrt app <operation>`.
70
70
 
71
71
  ## 3. Define the first object
72
72
 
73
- The generated `src/lib/objects/Item.ts` is one optional-tenant object with
74
- title, description, and status fields. Its small explicit `ItemCollection`
73
+ The generated `src/lib/objects/Note.ts` is one required-tenant object with
74
+ title and body fields. Its small explicit `NoteCollection`
75
75
  constructor keeps generated CLI/MCP runtime commands constructible. Its
76
76
  `@smrt()` configuration exposes CRUD to REST, MCP, WebMCP definitions, and CLI
77
77
  while using an API writable allowlist.
78
78
 
79
- Add objects beside Item and export them from `src/lib/objects/index.ts`.
79
+ Add objects beside Note and export them from `src/lib/objects/index.ts`.
80
80
  `vite.config.ts` already scans that directory.
81
81
 
82
82
  ## 4. Initialize or migrate the database
@@ -121,9 +121,9 @@ points.
121
121
 
122
122
  ## 7. Load data into a SvelteKit page
123
123
 
124
- The home page loads Item rows in `+page.server.ts`, returns plain serialized
125
- data, and declares `depends('smrt:items')`. The Svelte 5 page reads hydrated
126
- `$props()` data and calls `invalidate('smrt:items')` after a successful form
124
+ The home page loads Note rows in `+page.server.ts`, returns plain serialized
125
+ data, and declares `depends('smrt:notes')`. The Svelte 5 page reads hydrated
126
+ `$props()` data and calls `invalidate('smrt:notes')` after a successful form
127
127
  mutation. It does not fetch initial data in `onMount` or `$effect`.
128
128
 
129
129
  ## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
@@ -134,7 +134,7 @@ declared dev dependency:
134
134
 
135
135
  ```bash
136
136
  pnpm smrt objects
137
- pnpm smrt schema Item
137
+ pnpm smrt schema Note
138
138
  pnpm smrt generate-mcp --no-config --no-readme
139
139
  ```
140
140
 
@@ -156,7 +156,7 @@ is needed for WebMCP. Live browser collections remain opt-in per page; import
156
156
  the collection helpers only on pages that need interactive client data.
157
157
 
158
158
  The generated-project README shows the current hydration pattern:
159
- `createSmrtCollection(getCollectionDefinition('items'), { initialData,
159
+ `createSmrtCollection(getCollectionDefinition('notes'), { initialData,
160
160
  basePath: '/api' })` followed by `liveCollection()`. The root layout's
161
161
  read-only WebMCP registration does not materialize live collections.
162
162
 
@@ -1,4 +1,4 @@
1
1
  {
2
- "@happyvertical/smrt-app-mcp": "^0.55.16",
3
- "@happyvertical/smrt-mcp-openai": "^0.55.16"
2
+ "@happyvertical/smrt-app-mcp": "^0.55.18",
3
+ "@happyvertical/smrt-mcp-openai": "^0.55.18"
4
4
  }
@@ -2,17 +2,17 @@ import type { McpWorkflowToolDefinition } from '@happyvertical/smrt-app-mcp';
2
2
  import { mountMcpAppRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
3
3
  import { openAiDisplayMetadata, withOpenAiEntrypoints } from '@happyvertical/smrt-mcp-openai';
4
4
 
5
- import { Item } from '$lib/objects/Item';
5
+ import { Note } from '$lib/objects/Note';
6
6
  import { runtime } from '$lib/server/smrt';
7
7
 
8
- const ITEM_RESOURCE_URI = 'ui://smrt-app/v1/items.html';
9
- const itemResourceHtml = `<!doctype html><html lang="en"><head><title>Items</title></head><body><main tabindex="-1"><h1>Items</h1><p>Use the host controls to review your authorized items.</p></main></body></html>`;
8
+ const NOTE_RESOURCE_URI = 'ui://smrt-app/v1/notes.html';
9
+ const noteResourceHtml = `<!doctype html><html lang="en"><head><title>Notes</title></head><body><main tabindex="-1"><h1>Notes</h1><p>Use the host controls to review your authorized notes.</p></main></body></html>`;
10
10
 
11
- const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
11
+ const notesOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
12
12
  {
13
- name: 'items_overview',
13
+ name: 'notes_overview',
14
14
  description: 'Open the authorized item overview.',
15
- title: 'Items',
15
+ title: 'Notes',
16
16
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
17
17
  outputSchema: {
18
18
  type: 'object',
@@ -23,10 +23,10 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
23
23
  effect: 'read',
24
24
  idempotent: true,
25
25
  openWorld: false,
26
- ui: { resourceUri: ITEM_RESOURCE_URI },
26
+ ui: { resourceUri: NOTE_RESOURCE_URI },
27
27
  async execute() {
28
28
  return {
29
- content: [{ type: 'text', text: 'Item overview is ready.' }],
29
+ content: [{ type: 'text', text: 'Note overview is ready.' }],
30
30
  structuredContent: { state: 'ready' },
31
31
  };
32
32
  },
@@ -35,7 +35,7 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
35
35
  );
36
36
 
37
37
  /**
38
- * Stateless SDK-v2 Streamable HTTP MCP endpoint for this app's own `Item`.
38
+ * Stateless SDK-v2 Streamable HTTP MCP endpoint for this app's own `Note`.
39
39
  * `runtime` supplies the request database, the bearer adapter for the profile
40
40
  * it resolves per request, and the principal binding. Locally the principal
41
41
  * is the signed session (user, authorized tenant, and permission slugs), or
@@ -46,22 +46,22 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
46
46
  * `createHostedMcpResourceAuth({ ..., resolvePrincipal })` to override it).
47
47
  * A bearer principal is bound into the request permission
48
48
  * context (under `database-rls`, its own RLS transaction) with its scopes
49
- * capped to live permissions. Every principal needs `items.read`, so only
49
+ * capped to live permissions. Every principal needs `notes.read`, so only
50
50
  * read-only tools are published until per-operation authorization exists;
51
51
  * browser requests from a foreign `Origin` are refused.
52
52
  */
53
53
  export const POST = mountMcpAppRoute({
54
54
  runtime,
55
- models: [Item],
56
- requiredScopes: ['items.read'],
55
+ models: [Note],
56
+ requiredScopes: ['notes.read'],
57
57
  effects: ['read'],
58
- workflowTools: [itemsOverview],
58
+ workflowTools: [notesOverview],
59
59
  resources: [
60
60
  {
61
- uri: ITEM_RESOURCE_URI,
61
+ uri: NOTE_RESOURCE_URI,
62
62
  version: 'v1',
63
- name: 'Items',
64
- html: itemResourceHtml,
63
+ name: 'Notes',
64
+ html: noteResourceHtml,
65
65
  csp: {},
66
66
  metadata: openAiDisplayMetadata({ availableDisplayModes: ['inline'] }),
67
67
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-template-sveltekit",
3
- "version": "0.55.16",
3
+ "version": "0.55.18",
4
4
  "description": "Minimal SvelteKit project template with s-m-r-t framework integration",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -32,26 +32,26 @@
32
32
  "directory": "packages/template-sveltekit"
33
33
  },
34
34
  "peerDependencies": {
35
- "@happyvertical/smrt-core": "0.55.16"
35
+ "@happyvertical/smrt-core": "0.55.18"
36
36
  },
37
37
  "devDependencies": {
38
- "@happyvertical/smrt-app-cli": "0.55.16",
39
- "@happyvertical/smrt-app-mcp": "0.55.16",
40
- "@happyvertical/smrt-app-runtime": "0.55.16",
41
- "@happyvertical/smrt-assets": "0.55.16",
42
- "@happyvertical/smrt-chat": "0.55.16",
43
- "@happyvertical/smrt-cli": "0.55.16",
44
- "@happyvertical/smrt-config": "0.55.16",
45
- "@happyvertical/smrt-jobs": "0.55.16",
46
- "@happyvertical/smrt-mcp-openai": "0.55.16",
47
- "@happyvertical/smrt-playbooks": "0.55.16",
48
- "@happyvertical/smrt-profiles": "0.55.16",
49
- "@happyvertical/smrt-scanner": "0.55.16",
50
- "@happyvertical/smrt-svelte": "0.55.16",
51
- "@happyvertical/smrt-tenancy": "0.55.16",
52
- "@happyvertical/smrt-ui": "0.55.16",
53
- "@happyvertical/smrt-users": "0.55.16",
54
- "@happyvertical/smrt-web": "0.55.16",
38
+ "@happyvertical/smrt-app-cli": "0.55.18",
39
+ "@happyvertical/smrt-app-mcp": "0.55.18",
40
+ "@happyvertical/smrt-app-runtime": "0.55.18",
41
+ "@happyvertical/smrt-assets": "0.55.18",
42
+ "@happyvertical/smrt-chat": "0.55.18",
43
+ "@happyvertical/smrt-cli": "0.55.18",
44
+ "@happyvertical/smrt-config": "0.55.18",
45
+ "@happyvertical/smrt-jobs": "0.55.18",
46
+ "@happyvertical/smrt-mcp-openai": "0.55.18",
47
+ "@happyvertical/smrt-playbooks": "0.55.18",
48
+ "@happyvertical/smrt-profiles": "0.55.18",
49
+ "@happyvertical/smrt-scanner": "0.55.18",
50
+ "@happyvertical/smrt-svelte": "0.55.18",
51
+ "@happyvertical/smrt-tenancy": "0.55.18",
52
+ "@happyvertical/smrt-ui": "0.55.18",
53
+ "@happyvertical/smrt-users": "0.55.18",
54
+ "@happyvertical/smrt-web": "0.55.18",
55
55
  "@happyvertical/sql": "^0.89.12",
56
56
  "@modelcontextprotocol/client": "2.0.0",
57
57
  "@modelcontextprotocol/server": "^2.0.0",
@@ -1,7 +1,7 @@
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.43.10. It contains
4
- one object and a profile-aware operational surface for local, self-hosted, and
4
+ one Notes object and a profile-aware operational surface for local, self-hosted, and
5
5
  managed-cloud environments. It does not provision external providers.
6
6
 
7
7
  ## 1. Install and run
@@ -69,7 +69,7 @@ objects.
69
69
 
70
70
  ## 3. Define the first object
71
71
 
72
- `src/lib/objects/Item.ts` is the only example. It exposes the same CRUD action
72
+ `src/lib/objects/Note.ts` is the only example. It exposes the same CRUD action
73
73
  set to REST, MCP, WebMCP definitions, and the CLI, while limiting writable REST
74
74
  fields and opting into tenant scoping:
75
75
 
@@ -85,26 +85,25 @@ import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
85
85
  @smrt({
86
86
  api: {
87
87
  include: ['list', 'get', 'create', 'update', 'delete'],
88
- writable: ['title', 'description', 'status'],
88
+ writable: ['title', 'body'],
89
89
  },
90
90
  cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
91
91
  mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
92
92
  })
93
- @TenantScoped({ mode: 'optional' })
94
- export class Item extends SmrtObject {
95
- @tenantId({ nullable: true })
96
- tenantId: string | null = null;
93
+ @TenantScoped({ mode: 'required' })
94
+ export class Note extends SmrtObject {
95
+ @tenantId()
96
+ tenantId: string = '';
97
97
 
98
98
  title: string = '';
99
- description: string = '';
100
- status: string = 'draft';
99
+ body: string = '';
101
100
  }
102
101
 
103
- export class ItemCollection extends SmrtCollection<Item> {
104
- static readonly _itemClass = Item;
102
+ export class NoteCollection extends SmrtCollection<Note> {
103
+ static readonly _itemClass = Note;
105
104
  }
106
105
 
107
- ObjectRegistry.registerCollection('Item', ItemCollection);
106
+ ObjectRegistry.registerCollection('Note', NoteCollection);
108
107
  ```
109
108
 
110
109
  Add another class beside it and export the class from
@@ -182,8 +181,8 @@ These are separate records with separate responsibilities:
182
181
  - Session binds the authenticated user to an active tenant and publishes the
183
182
  resolved permission set for the request.
184
183
 
185
- CRUD permissions are manifest-derived: `items.read`, `items.create`,
186
- `items.update`, and `items.delete`. Provisioning code should sync the catalog
184
+ CRUD permissions are manifest-derived: `notes.read`, `notes.create`,
185
+ `notes.update`, and `notes.delete`. Provisioning code should sync the catalog
187
186
  and seed roles after the database migration:
188
187
 
189
188
  ```ts
@@ -219,17 +218,17 @@ session context directly. Return plain serializable rows:
219
218
 
220
219
  ```ts
221
220
  export const load: PageServerLoad = async ({ depends, locals }) => {
222
- depends('smrt:items');
221
+ depends('smrt:notes');
223
222
 
224
- if (!locals.permissions.includes('items.read')) {
225
- return { items: [] };
223
+ if (!locals.permissions.includes('notes.read')) {
224
+ return { notes: [] };
226
225
  }
227
226
 
228
- const items = await runtime.getCollection<Item>('Item');
229
- const rows = await items.list({ limit: 50 });
227
+ const notes = await runtime.getCollection<Note>('Note');
228
+ const rows = await notes.list({ limit: 50 });
230
229
  return {
231
- items: rows.flatMap((item) =>
232
- item.id ? [{ id: item.id, title: item.title, status: item.status }] : [],
230
+ notes: rows.flatMap((note) =>
231
+ note.id ? [{ id: note.id, title: note.title, body: note.body }] : [],
233
232
  ),
234
233
  };
235
234
  };
@@ -237,8 +236,8 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
237
236
 
238
237
  The page receives that data through `$props()`. Do not fetch initial page data
239
238
  from `onMount` or `$effect`; SvelteKit already serialized it into the response.
240
- After a mutation, call `invalidate('smrt:items')`. Only loads that declared
241
- `depends('smrt:items')` re-run.
239
+ After a mutation, call `invalidate('smrt:notes')`. Only loads that declared
240
+ `depends('smrt:notes')` re-run.
242
241
 
243
242
  ## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
244
243
 
@@ -256,8 +255,8 @@ include malformed manifest content.
256
255
 
257
256
  The option stages `src/routes/api/mcp/+server.ts`, one
258
257
  `mountMcpAppRoute({ runtime, ... })` call from
259
- `@happyvertical/smrt-app-mcp/sveltekit` that publishes `Item` to
260
- principals holding `items.read` (the signed session's permissions locally),
258
+ `@happyvertical/smrt-app-mcp/sveltekit` that publishes `Note` to
259
+ principals holding `notes.read` (the signed session's permissions locally),
261
260
  refuses browser requests from a foreign `Origin`, and serves a bounded static
262
261
  resource with a restrictive CSP and the optional OpenAI display metadata. It
263
262
  also stages the protected-resource metadata route. For a view, mount
@@ -273,20 +272,20 @@ request nor grants tenant or workflow authority. Register `.app.json` only
273
272
  after the host creates an eligible server ID; do not copy runtime extension
274
273
  metadata into the portable manifest.
275
274
 
276
- The Item configuration generates:
275
+ The Note configuration generates:
277
276
 
278
- - REST: `GET`/`POST /api/items` and
279
- `GET`/`PUT`/`DELETE /api/items/[id]`.
280
- - MCP descriptors and tools for Item CRUD.
277
+ - REST: `GET`/`POST /api/notes` and
278
+ `GET`/`PUT`/`DELETE /api/notes/[id]`.
279
+ - MCP descriptors and tools for Note CRUD.
281
280
  - Web collection definitions in the virtual
282
281
  `@happyvertical/smrt-virt-web` module.
283
- - CLI commands for Item CRUD.
282
+ - CLI commands for Note CRUD.
284
283
 
285
284
  Inspect the registered objects and use the example CLI:
286
285
 
287
286
  ```bash
288
287
  pnpm smrt objects
289
- pnpm smrt schema Item
288
+ pnpm smrt schema Note
290
289
  ```
291
290
 
292
291
  The CLI's manifest-only `objects` and `schema` commands work directly in this
@@ -372,12 +371,12 @@ hydrated rows so the first render does not issue a duplicate request:
372
371
 
373
372
  let { data }: PageProps = $props();
374
373
 
375
- const items = createSmrtCollection(getCollectionDefinition('items'), {
374
+ const notes = createSmrtCollection(getCollectionDefinition('notes'), {
376
375
  basePath: '/api',
377
- initialData: data.items,
376
+ initialData: data.notes,
378
377
  staleTimeMs: 30_000,
379
378
  });
380
- const view = liveCollection(items);
379
+ const view = liveCollection(notes);
381
380
  </script>
382
381
 
383
382
  {#each view.rows as item (item.id)}
@@ -441,7 +440,7 @@ the signed browser session. The other is an owner-minted bearer token for a
441
440
  local MCP client such as Claude Desktop through `smrt-mcp-bridge`:
442
441
 
443
442
  ```bash
444
- pnpm exec smrt app token --scopes items.read --label "Claude Desktop"
443
+ pnpm exec smrt app token --scopes notes.read --label "Claude Desktop"
445
444
  pnpm exec smrt app token list
446
445
  pnpm exec smrt app token revoke <id>
447
446
  ```
@@ -39,19 +39,19 @@
39
39
  "svelte-check": "^4.7.6",
40
40
  "typescript": "6.0.3",
41
41
  "vite": "8.3.0",
42
- "@happyvertical/smrt-cli": "^0.55.16"
42
+ "@happyvertical/smrt-cli": "^0.55.18"
43
43
  },
44
44
  "dependencies": {
45
- "@happyvertical/smrt-app-runtime": "^0.55.16",
46
- "@happyvertical/smrt-config": "^0.55.16",
47
- "@happyvertical/smrt-core": "^0.55.16",
48
- "@happyvertical/smrt-jobs": "^0.55.16",
49
- "@happyvertical/smrt-profiles": "^0.55.16",
50
- "@happyvertical/smrt-svelte": "^0.55.16",
51
- "@happyvertical/smrt-tenancy": "^0.55.16",
52
- "@happyvertical/smrt-ui": "^0.55.16",
53
- "@happyvertical/smrt-users": "^0.55.16",
54
- "@happyvertical/smrt-web": "^0.55.16",
45
+ "@happyvertical/smrt-app-runtime": "^0.55.18",
46
+ "@happyvertical/smrt-config": "^0.55.18",
47
+ "@happyvertical/smrt-core": "^0.55.18",
48
+ "@happyvertical/smrt-jobs": "^0.55.18",
49
+ "@happyvertical/smrt-profiles": "^0.55.18",
50
+ "@happyvertical/smrt-svelte": "^0.55.18",
51
+ "@happyvertical/smrt-tenancy": "^0.55.18",
52
+ "@happyvertical/smrt-ui": "^0.55.18",
53
+ "@happyvertical/smrt-users": "^0.55.18",
54
+ "@happyvertical/smrt-web": "^0.55.18",
55
55
  "@happyvertical/sql": "^0.89.12",
56
56
  "@modelcontextprotocol/server": "^2.0.0"
57
57
  }
@@ -2,7 +2,11 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
8
+ <meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)" />
9
+ <link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
6
10
  %sveltekit.head%
7
11
  </head>
8
12
  <body data-sveltekit-preload-data="hover">
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Example s-m-r-t object.
3
+ *
4
+ * This one declaration is the app's data model: the `smrt()` Vite plugin turns
5
+ * it into the schema, the REST routes under `/api/notes`, the WebMCP read
6
+ * tools, and the generated CLI/MCP commands. Rename or replace it with your
7
+ * own objects.
8
+ */
9
+
10
+ import {
11
+ ObjectRegistry,
12
+ SmrtCollection,
13
+ SmrtObject,
14
+ smrt,
15
+ } from '@happyvertical/smrt-core';
16
+ import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
17
+
18
+ @smrt({
19
+ api: {
20
+ include: ['list', 'get', 'create', 'update', 'delete'],
21
+ writable: ['title', 'body'],
22
+ },
23
+ cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
24
+ mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
25
+ })
26
+ @TenantScoped({ mode: 'required' })
27
+ export class Note extends SmrtObject {
28
+ /** Tenant rows are isolated automatically by the session's tenant. */
29
+ @tenantId()
30
+ tenantId: string = '';
31
+
32
+ /** Note title */
33
+ title: string = '';
34
+
35
+ /** Note body */
36
+ body: string = '';
37
+ }
38
+
39
+ /** Explicit collection constructor used by generated CLI/MCP/runtime tools. */
40
+ export class NoteCollection extends SmrtCollection<Note> {
41
+ static readonly _itemClass = Note;
42
+ }
43
+
44
+ ObjectRegistry.registerCollection('Note', NoteCollection);
@@ -5,4 +5,4 @@
5
5
  * They will be automatically registered and available for CLI/API/MCP.
6
6
  */
7
7
 
8
- export { Item, ItemCollection } from './Item.js';
8
+ export { Note, NoteCollection } from './Note.js';
@@ -0,0 +1,19 @@
1
+ import { goto, invalidateAll } from '$app/navigation';
2
+
3
+ /**
4
+ * Sign out through the runtime's same-origin `POST /sign-out` route, then
5
+ * refresh the session-derived layout data and return home. A failed request
6
+ * (403, 5xx) throws instead of pretending the session ended: the user stays
7
+ * signed in and the error names the status.
8
+ */
9
+ export async function signOut(): Promise<void> {
10
+ const response = await fetch('/sign-out', {
11
+ method: 'POST',
12
+ credentials: 'same-origin',
13
+ });
14
+ if (!response.ok) {
15
+ throw new Error(`Sign-out failed with status ${response.status}`);
16
+ }
17
+ await invalidateAll();
18
+ await goto('/');
19
+ }
@@ -3,6 +3,7 @@
3
3
  import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
4
4
  import { AppShell } from '@happyvertical/smrt-svelte/app';
5
5
  import type { ShellNavItem } from '@happyvertical/smrt-svelte/workspace';
6
+ import { signOut } from '$lib/sign-out';
6
7
  import type { LayoutProps } from './$types';
7
8
 
8
9
  let { data, children }: LayoutProps = $props();
@@ -10,8 +11,8 @@
10
11
  // Add application routes here. Generated REST routes live under /api and do
11
12
  // not automatically imply a human-facing page.
12
13
  const nav: ShellNavItem[] = [
13
- { href: '/', label: 'Items', description: 'The example s-m-r-t object' },
14
- { href: '/settings', label: 'Settings', description: 'Workspace layout and shortcuts' },
14
+ { href: '/', label: 'Notes', icon: 'book', description: 'Your workspace notes' },
15
+ { href: '/settings', label: 'Settings', icon: 'settings', description: 'Workspace layout and shortcuts' },
15
16
  ];
16
17
 
17
18
  // Generated read-only WebMCP tools, only in browsers that expose WebMCP.
@@ -24,18 +25,17 @@
24
25
 
25
26
  <AppShell
26
27
  title="s-m-r-t app"
27
- subtitle="SvelteKit"
28
28
  {webmcp}
29
29
  {nav}
30
30
  currentHref={page.url.pathname}
31
31
  runtimeDiagnostics={data.session.authenticated}
32
- tenantLabel={data.session.activeTenantId ? 'Authorized session tenant' : 'No active tenant'}
33
32
  settingsHref="/settings"
33
+ edgeToggles={{ left: true }}
34
+ config={{ left: { initial: 'expanded' } }}
35
+ user={data.session.user}
36
+ onSignOut={signOut}
37
+ phoneTabBar
38
+ chrome={!page.route.id?.startsWith('/setup')}
34
39
  >
35
- {#snippet appPanelDocs()}
36
- {#if data.session.selectedTenantSlug}
37
- <p>Selected URL tenant: <strong>{data.session.selectedTenantSlug}</strong></p>
38
- {/if}
39
- {/snippet}
40
40
  {@render children()}
41
41
  </AppShell>
@@ -1,21 +1,21 @@
1
1
  import { assertOperationPermission } from '@happyvertical/smrt-users';
2
2
  import { fail } from '@sveltejs/kit';
3
3
 
4
- import type { Item } from '$lib/objects/Item';
4
+ import type { Note } from '$lib/objects/Note';
5
5
  import { runtime } from '$lib/server/smrt';
6
6
  import type { Actions, PageServerLoad } from './$types';
7
7
 
8
- const READ_PERMISSION = 'items.read';
9
- const CREATE_PERMISSION = 'items.create';
8
+ const READ_PERMISSION = 'notes.read';
9
+ const CREATE_PERMISSION = 'notes.create';
10
10
 
11
- export interface ItemRow {
11
+ export interface NoteRow {
12
12
  id: string;
13
13
  title: string;
14
- status: string;
14
+ body: string;
15
15
  }
16
16
 
17
17
  export const load: PageServerLoad = async ({ depends, locals }) => {
18
- depends('smrt:items');
18
+ depends('smrt:notes');
19
19
 
20
20
  const authenticated = Boolean(locals.user);
21
21
  const hasTenant = Boolean(locals.tenantId);
@@ -26,8 +26,9 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
26
26
 
27
27
  if (!canRead) {
28
28
  return {
29
- items: [] as ItemRow[],
29
+ notes: [] as NoteRow[],
30
30
  canCreate,
31
+ authenticated,
31
32
  loadError: null as string | null,
32
33
  accessMessage: !authenticated
33
34
  ? 'Sign in before loading tenant data.'
@@ -38,28 +39,30 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
38
39
  }
39
40
 
40
41
  try {
41
- const items = await runtime.getCollection<Item>('Item');
42
- const rows = await items.list({
42
+ const notes = await runtime.getCollection<Note>('Note');
43
+ const rows = await notes.list({
43
44
  orderBy: 'created_at DESC',
44
- limit: 50,
45
+ limit: 100,
45
46
  });
46
47
 
47
48
  return {
48
- items: rows.flatMap((item): ItemRow[] =>
49
- item.id
50
- ? [{ id: item.id, title: item.title, status: item.status }]
49
+ notes: rows.flatMap((note): NoteRow[] =>
50
+ note.id
51
+ ? [{ id: note.id, title: note.title, body: note.body }]
51
52
  : [],
52
53
  ),
53
54
  canCreate,
55
+ authenticated,
54
56
  loadError: null as string | null,
55
57
  accessMessage: null as string | null,
56
58
  };
57
59
  } catch (error) {
58
60
  return {
59
- items: [] as ItemRow[],
61
+ notes: [] as NoteRow[],
60
62
  canCreate,
63
+ authenticated,
61
64
  loadError:
62
- error instanceof Error ? error.message : 'Failed to load items',
65
+ error instanceof Error ? error.message : 'Failed to load notes',
63
66
  accessMessage: null as string | null,
64
67
  };
65
68
  }
@@ -72,9 +75,9 @@ export const actions: Actions = {
72
75
  }
73
76
 
74
77
  const decision = await assertOperationPermission({
75
- ...runtime.classOptions('Item'),
78
+ ...runtime.classOptions('Note'),
76
79
  action: 'create',
77
- collection: 'items',
80
+ collection: 'notes',
78
81
  onDeny: 'return',
79
82
  permissionSet: locals.permissions,
80
83
  tenantId: locals.tenantId,
@@ -86,18 +89,20 @@ export const actions: Actions = {
86
89
 
87
90
  const form = await request.formData();
88
91
  const title = String(form.get('title') ?? '').trim();
92
+ const body = String(form.get('body') ?? '').trim();
89
93
  if (!title) {
90
- return fail(400, { title, error: 'Title is required.' });
94
+ return fail(400, { title, body, error: 'A title is required.' });
91
95
  }
92
96
 
93
97
  try {
94
- const items = await runtime.getCollection<Item>('Item');
95
- await items.create({ title });
98
+ const notes = await runtime.getCollection<Note>('Note');
99
+ await notes.create({ title, body });
96
100
  return { created: title };
97
101
  } catch (error) {
98
102
  return fail(500, {
99
103
  title,
100
- error: error instanceof Error ? error.message : 'Failed to create item',
104
+ body,
105
+ error: error instanceof Error ? error.message : 'Failed to create note',
101
106
  });
102
107
  }
103
108
  },
@@ -1,213 +1,137 @@
1
1
  <script lang="ts">
2
2
  import { enhance } from '$app/forms';
3
3
  import { invalidate } from '$app/navigation';
4
+ import { Form, FormGroup, Input, Textarea } from '@happyvertical/smrt-ui/forms';
5
+ import { EmptyState } from '@happyvertical/smrt-ui/layout';
6
+ import { Button, Card } from '@happyvertical/smrt-ui/ui';
7
+ import type { SubmitFunction } from '@sveltejs/kit';
8
+ import { fromAction } from 'svelte/attachments';
4
9
  import type { PageProps } from './$types';
5
10
 
6
11
  let { data, form }: PageProps = $props();
7
12
  let creating = $state(false);
13
+ let title = $state('');
14
+ let body = $state('');
15
+ let composing = $state(false);
16
+
17
+ // The form shows once the owner has notes, or after "Start your first note".
18
+ const showForm = $derived(
19
+ data.canCreate && (data.notes.length > 0 || composing),
20
+ );
21
+
22
+ const submit: SubmitFunction = () => {
23
+ creating = true;
24
+ return async ({ result, update }) => {
25
+ try {
26
+ await update({ invalidateAll: false, reset: false });
27
+ if (result.type === 'success') {
28
+ title = '';
29
+ body = '';
30
+ await invalidate('smrt:notes');
31
+ }
32
+ } finally {
33
+ creating = false;
34
+ }
35
+ };
36
+ };
8
37
  </script>
9
38
 
10
39
  <svelte:head>
11
- <title>s-m-r-t SvelteKit starter</title>
40
+ <title>Notes</title>
12
41
  </svelte:head>
13
42
 
14
- <div class="page">
15
- <header>
16
- <p class="eyebrow">s-m-r-t 0.38.25</p>
17
- <h1>Learn the application foundation</h1>
18
- <p>
19
- One object, SQLite defaults, server-loaded data, generated interfaces,
20
- session-authorized tenancy, and room to grow.
21
- </p>
22
- </header>
43
+ <main class="page">
44
+ <h1>Notes</h1>
23
45
 
24
- <section>
25
- <div class="section-heading">
26
- <div>
27
- <h2>Items</h2>
28
- <p>The initial list comes from <code>+page.server.ts</code>.</p>
29
- </div>
30
- </div>
31
-
32
- {#if data.loadError}
33
- <p class="notice notice--error">{data.loadError}</p>
46
+ {#if data.loadError}
47
+ <Card>
48
+ <p role="alert">{data.loadError}</p>
34
49
  <p>Run <code>pnpm db:migrate</code> after changing object fields.</p>
35
- {:else if data.accessMessage}
36
- <p class="notice">{data.accessMessage}</p>
37
- {:else if data.items.length === 0}
38
- <p class="notice">No items yet.</p>
50
+ </Card>
51
+ {:else if data.accessMessage}
52
+ <Card>
53
+ <p>{data.accessMessage}</p>
54
+ {#if !data.authenticated}
55
+ <Button href="/setup">Set up this app</Button>
56
+ {/if}
57
+ </Card>
58
+ {:else}
59
+ {#if showForm}
60
+ <Card>
61
+ <Form
62
+ method="POST"
63
+ action="?/create"
64
+ preventDefault={false}
65
+ {@attach fromAction(enhance, () => submit)}
66
+ >
67
+ <FormGroup label="Title" required>
68
+ <Input name="title" bind:value={title} required />
69
+ </FormGroup>
70
+ <FormGroup label="Body">
71
+ <Textarea name="body" bind:value={body} rows={4} />
72
+ </FormGroup>
73
+ {#if form?.error}<p role="alert">{form.error}</p>{/if}
74
+ <div>
75
+ <Button type="submit" loading={creating} disabled={creating}>
76
+ Add note
77
+ </Button>
78
+ </div>
79
+ </Form>
80
+ </Card>
81
+ {/if}
82
+
83
+ {#if data.notes.length === 0}
84
+ <EmptyState
85
+ title="Start your first note"
86
+ description={data.canCreate
87
+ ? 'Notes you add show up here for everyone in this workspace.'
88
+ : 'Notes will show up here once someone adds them.'}
89
+ actionLabel={data.canCreate && !composing ? 'New note' : undefined}
90
+ onaction={() => {
91
+ composing = true;
92
+ }}
93
+ />
39
94
  {:else}
40
- <ul class="items">
41
- {#each data.items as item (item.id)}
95
+ <ul class="notes">
96
+ {#each data.notes as note (note.id)}
42
97
  <li>
43
- <strong>{item.title}</strong>
44
- <span>{item.status}</span>
98
+ <Card>
99
+ <h2>{note.title}</h2>
100
+ {#if note.body}<p>{note.body}</p>{/if}
101
+ </Card>
45
102
  </li>
46
103
  {/each}
47
104
  </ul>
48
105
  {/if}
49
-
50
- {#if data.canCreate}
51
- <form
52
- method="POST"
53
- action="?/create"
54
- use:enhance={() => {
55
- creating = true;
56
- return async ({ result, update }) => {
57
- try {
58
- await update({ invalidateAll: false });
59
- if (result.type === 'success') {
60
- await invalidate('smrt:items');
61
- }
62
- } finally {
63
- creating = false;
64
- }
65
- };
66
- }}
67
- >
68
- <label for="title">New item</label>
69
- <div class="form-row">
70
- <input id="title" name="title" placeholder="Write a title" required />
71
- <button type="submit" disabled={creating}>
72
- {creating ? 'Creating…' : 'Create item'}
73
- </button>
74
- </div>
75
- </form>
76
- {/if}
77
-
78
- {#if form?.error}
79
- <p class="notice notice--error">{form.error}</p>
80
- {/if}
81
- </section>
82
-
83
- <section>
84
- <h2>Where to extend</h2>
85
- <ol>
86
- <li>Edit <code>src/lib/objects/Item.ts</code>.</li>
87
- <li>Run <code>pnpm db:migrate</code>.</li>
88
- <li>Add pages that query collections in <code>+page.server.ts</code>.</li>
89
- <li>Add membership-gated login and tenant-switch actions for your app.</li>
90
- </ol>
91
- </section>
92
- </div>
106
+ {/if}
107
+ </main>
93
108
 
94
109
  <style>
95
- .page {
96
- display: grid;
97
- gap: var(--smrt-spacing-6);
98
- width: min(100%, 56rem);
99
- margin-inline: auto;
100
- padding: var(--smrt-spacing-8);
101
- }
102
-
103
- header,
104
- section,
105
- .section-heading,
106
- form {
107
- display: grid;
108
- gap: var(--smrt-spacing-3);
109
- }
110
-
111
- header p,
112
- h1,
113
- h2,
114
- section p {
115
- margin: 0;
116
- }
117
-
118
- .eyebrow {
119
- color: var(--smrt-color-primary);
120
- font: var(--smrt-typography-label-large);
121
- letter-spacing: 0.08em;
122
- text-transform: uppercase;
123
- }
124
-
125
- section {
126
- padding: var(--smrt-spacing-6);
127
- border: 1px solid var(--smrt-color-outline-variant);
128
- border-radius: var(--smrt-radius-large);
129
- background: var(--smrt-color-surface);
130
- }
131
-
132
- .notice {
133
- padding: var(--smrt-spacing-3);
134
- border-radius: var(--smrt-radius-medium);
135
- background: var(--smrt-color-surface-container);
136
- color: var(--smrt-color-on-surface-variant);
137
- }
138
-
139
- .notice--error {
140
- color: var(--smrt-color-error);
141
- }
142
-
143
- .items {
144
- display: grid;
145
- gap: var(--smrt-spacing-2);
146
- margin: 0;
147
- padding: 0;
148
- list-style: none;
149
- }
150
-
151
- .items li {
152
- display: flex;
153
- justify-content: space-between;
154
- gap: var(--smrt-spacing-3);
155
- padding-block: var(--smrt-spacing-2);
156
- border-block-end: 1px solid var(--smrt-color-outline-variant);
157
- }
158
-
159
- .items span {
160
- color: var(--smrt-color-on-surface-variant);
161
- }
162
-
163
- label {
164
- font-weight: var(--smrt-typography-weight-semibold);
165
- }
166
-
167
- .form-row {
168
- display: flex;
169
- gap: var(--smrt-spacing-2);
170
- }
171
-
172
- input,
173
- button {
174
- min-height: 2.75rem;
175
- padding-inline: var(--smrt-spacing-3);
176
- border-radius: var(--smrt-radius-medium);
177
- font: inherit;
178
- }
179
-
180
- input {
181
- flex: 1;
182
- border: 1px solid var(--smrt-color-outline);
183
- background: var(--smrt-color-surface);
184
- color: var(--smrt-color-on-surface);
185
- }
186
-
187
- button {
188
- border: 0;
189
- background: var(--smrt-color-primary);
190
- color: var(--smrt-color-on-primary);
191
- cursor: pointer;
192
- }
193
-
194
- button:disabled {
195
- opacity: 0.6;
196
- cursor: wait;
197
- }
110
+ @layer smrt.theme {
111
+ .page {
112
+ display: grid;
113
+ gap: var(--smrt-spacing-6);
114
+ width: min(100%, 48rem);
115
+ margin-inline: auto;
116
+ padding: var(--smrt-spacing-6) var(--smrt-spacing-4);
117
+ }
198
118
 
199
- code {
200
- font-family: var(--smrt-typography-code-font-family, monospace);
201
- }
119
+ .notes {
120
+ display: grid;
121
+ gap: var(--smrt-spacing-3);
122
+ margin: 0;
123
+ padding: 0;
124
+ list-style: none;
125
+ }
202
126
 
203
- @media (max-width: 42rem) {
204
- .page {
205
- padding: var(--smrt-spacing-4);
127
+ h1,
128
+ h2,
129
+ p {
130
+ margin: 0;
206
131
  }
207
132
 
208
- .form-row {
209
- align-items: stretch;
210
- flex-direction: column;
133
+ h2 {
134
+ font: var(--smrt-typography-title-medium);
211
135
  }
212
136
  }
213
137
  </style>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { ShellSettingsPage } from '@happyvertical/smrt-svelte/app';
3
+ import { signOut } from '$lib/sign-out';
3
4
  </script>
4
5
 
5
6
  <svelte:head>
@@ -7,4 +8,4 @@
7
8
  </svelte:head>
8
9
 
9
10
  <!-- Panel layout and WASD hotkeys, persisted through the shell's storageKey. -->
10
- <ShellSettingsPage />
11
+ <ShellSettingsPage onSignOut={signOut} />
@@ -8,4 +8,4 @@
8
8
 
9
9
  <svelte:head><title>Set up your local application</title></svelte:head>
10
10
 
11
- <OwnerSetupForm {data} {form} {enhance} />
11
+ <OwnerSetupForm {data} {form} {enhance} title="s-m-r-t app" />
@@ -0,0 +1,6 @@
1
+ import { createSignOutHandler } from '@happyvertical/smrt-app-runtime/sveltekit';
2
+
3
+ import { runtime } from '$lib/server/smrt';
4
+
5
+ /** POST-only, same-origin: destroys the session and clears its cookie. */
6
+ export const POST = createSignOutHandler(runtime);
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><style>svg{color:#1c1b1f}@media(prefers-color-scheme:dark){svg{color:#e6e1e5}}</style><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9 13h7M9 17h5"/></svg>
@@ -19,22 +19,22 @@ export default {
19
19
 
20
20
  // Dependencies to add to generated project
21
21
  dependencies: {
22
- '@happyvertical/smrt-app-runtime': '^0.55.16',
23
- '@happyvertical/smrt-config': '^0.55.16',
24
- '@happyvertical/smrt-core': '^0.55.16',
25
- '@happyvertical/smrt-jobs': '^0.55.16',
26
- '@happyvertical/smrt-profiles': '^0.55.16',
27
- '@happyvertical/smrt-svelte': '^0.55.16',
28
- '@happyvertical/smrt-tenancy': '^0.55.16',
29
- '@happyvertical/smrt-ui': '^0.55.16',
30
- '@happyvertical/smrt-users': '^0.55.16',
31
- '@happyvertical/smrt-web': '^0.55.16',
22
+ '@happyvertical/smrt-app-runtime': '^0.55.18',
23
+ '@happyvertical/smrt-config': '^0.55.18',
24
+ '@happyvertical/smrt-core': '^0.55.18',
25
+ '@happyvertical/smrt-jobs': '^0.55.18',
26
+ '@happyvertical/smrt-profiles': '^0.55.18',
27
+ '@happyvertical/smrt-svelte': '^0.55.18',
28
+ '@happyvertical/smrt-tenancy': '^0.55.18',
29
+ '@happyvertical/smrt-ui': '^0.55.18',
30
+ '@happyvertical/smrt-users': '^0.55.18',
31
+ '@happyvertical/smrt-web': '^0.55.18',
32
32
  '@happyvertical/sql': '^0.89.12',
33
33
  '@modelcontextprotocol/server': '^2.0.0',
34
34
  },
35
35
 
36
36
  devDependencies: {
37
- '@happyvertical/smrt-cli': '^0.55.16',
37
+ '@happyvertical/smrt-cli': '^0.55.18',
38
38
  '@sveltejs/adapter-node': '^5.5.7',
39
39
  '@sveltejs/kit': '2.70.3',
40
40
  '@sveltejs/vite-plugin-svelte': '^7.3.0',
@@ -1,45 +0,0 @@
1
- /**
2
- * Example s-m-r-t object.
3
- *
4
- * This is a sample SMRT object to demonstrate the pattern.
5
- * Rename or replace this with your own objects.
6
- */
7
-
8
- import {
9
- ObjectRegistry,
10
- SmrtCollection,
11
- SmrtObject,
12
- smrt,
13
- } from '@happyvertical/smrt-core';
14
- import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
15
-
16
- @smrt({
17
- api: {
18
- include: ['list', 'get', 'create', 'update', 'delete'],
19
- writable: ['title', 'description', 'status'],
20
- },
21
- cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
22
- mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
23
- })
24
- @TenantScoped({ mode: 'optional' })
25
- export class Item extends SmrtObject {
26
- /** Null rows are global; tenant rows are isolated automatically. */
27
- @tenantId({ nullable: true })
28
- tenantId: string | null = null;
29
-
30
- /** Item title */
31
- title: string = '';
32
-
33
- /** Item description */
34
- description: string = '';
35
-
36
- /** Status of the item */
37
- status: string = 'draft';
38
- }
39
-
40
- /** Explicit collection constructor used by generated CLI/MCP/runtime tools. */
41
- export class ItemCollection extends SmrtCollection<Item> {
42
- static readonly _itemClass = Item;
43
- }
44
-
45
- ObjectRegistry.registerCollection('Item', ItemCollection);