create-appystack 0.1.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.
Files changed (99) hide show
  1. package/README.md +54 -0
  2. package/bin/index.js +243 -0
  3. package/package.json +39 -0
  4. package/template/.claude/skills/recipe/SKILL.md +71 -0
  5. package/template/.claude/skills/recipe/domains/care-provider-operations.md +185 -0
  6. package/template/.claude/skills/recipe/domains/youtube-launch-optimizer.md +154 -0
  7. package/template/.claude/skills/recipe/references/file-crud.md +295 -0
  8. package/template/.claude/skills/recipe/references/nav-shell.md +233 -0
  9. package/template/.dockerignore +39 -0
  10. package/template/.env.example +13 -0
  11. package/template/.github/workflows/ci.yml +43 -0
  12. package/template/.husky/pre-commit +1 -0
  13. package/template/.prettierignore +7 -0
  14. package/template/.prettierrc +8 -0
  15. package/template/.vscode/launch.json +59 -0
  16. package/template/CLAUDE.md +114 -0
  17. package/template/Dockerfile +56 -0
  18. package/template/README.md +219 -0
  19. package/template/client/index.html +13 -0
  20. package/template/client/package.json +43 -0
  21. package/template/client/src/App.test.tsx +67 -0
  22. package/template/client/src/App.tsx +11 -0
  23. package/template/client/src/components/ErrorFallback.test.tsx +64 -0
  24. package/template/client/src/components/ErrorFallback.tsx +18 -0
  25. package/template/client/src/config/env.test.ts +64 -0
  26. package/template/client/src/config/env.ts +34 -0
  27. package/template/client/src/contexts/AppContext.test.tsx +81 -0
  28. package/template/client/src/contexts/AppContext.tsx +52 -0
  29. package/template/client/src/demo/ContactForm.test.tsx +97 -0
  30. package/template/client/src/demo/ContactForm.tsx +100 -0
  31. package/template/client/src/demo/DemoPage.tsx +56 -0
  32. package/template/client/src/demo/SocketDemo.test.tsx +160 -0
  33. package/template/client/src/demo/SocketDemo.tsx +65 -0
  34. package/template/client/src/demo/StatusGrid.test.tsx +181 -0
  35. package/template/client/src/demo/StatusGrid.tsx +77 -0
  36. package/template/client/src/demo/TechStackDisplay.test.tsx +63 -0
  37. package/template/client/src/demo/TechStackDisplay.tsx +75 -0
  38. package/template/client/src/hooks/useServerStatus.test.ts +133 -0
  39. package/template/client/src/hooks/useServerStatus.ts +67 -0
  40. package/template/client/src/hooks/useSocket.test.ts +152 -0
  41. package/template/client/src/hooks/useSocket.ts +43 -0
  42. package/template/client/src/lib/utils.test.ts +33 -0
  43. package/template/client/src/lib/utils.ts +14 -0
  44. package/template/client/src/main.test.tsx +113 -0
  45. package/template/client/src/main.tsx +14 -0
  46. package/template/client/src/pages/LandingPage.test.tsx +30 -0
  47. package/template/client/src/pages/LandingPage.tsx +29 -0
  48. package/template/client/src/styles/index.css +50 -0
  49. package/template/client/src/test/msw/browser.ts +4 -0
  50. package/template/client/src/test/msw/handlers.ts +12 -0
  51. package/template/client/src/test/msw/msw-example.test.ts +69 -0
  52. package/template/client/src/test/msw/server.ts +14 -0
  53. package/template/client/src/test/setup.ts +10 -0
  54. package/template/client/src/utils/api.test.ts +79 -0
  55. package/template/client/src/utils/api.ts +42 -0
  56. package/template/client/src/vite-env.d.ts +13 -0
  57. package/template/client/tsconfig.json +17 -0
  58. package/template/client/vite.config.ts +38 -0
  59. package/template/client/vitest.config.ts +36 -0
  60. package/template/docker-compose.yml +19 -0
  61. package/template/e2e/smoke.test.ts +95 -0
  62. package/template/e2e/socket.test.ts +96 -0
  63. package/template/eslint.config.js +2 -0
  64. package/template/package.json +50 -0
  65. package/template/playwright.config.ts +14 -0
  66. package/template/scripts/customize.ts +175 -0
  67. package/template/server/nodemon.json +5 -0
  68. package/template/server/package.json +45 -0
  69. package/template/server/src/app.test.ts +103 -0
  70. package/template/server/src/config/env.test.ts +97 -0
  71. package/template/server/src/config/env.ts +29 -0
  72. package/template/server/src/config/logger.test.ts +58 -0
  73. package/template/server/src/config/logger.ts +17 -0
  74. package/template/server/src/helpers/response.test.ts +53 -0
  75. package/template/server/src/helpers/response.ts +17 -0
  76. package/template/server/src/index.ts +118 -0
  77. package/template/server/src/middleware/errorHandler.test.ts +84 -0
  78. package/template/server/src/middleware/errorHandler.ts +27 -0
  79. package/template/server/src/middleware/rateLimiter.test.ts +68 -0
  80. package/template/server/src/middleware/rateLimiter.ts +8 -0
  81. package/template/server/src/middleware/requestLogger.test.ts +111 -0
  82. package/template/server/src/middleware/requestLogger.ts +17 -0
  83. package/template/server/src/middleware/validate.test.ts +213 -0
  84. package/template/server/src/middleware/validate.ts +23 -0
  85. package/template/server/src/routes/health.test.ts +17 -0
  86. package/template/server/src/routes/health.ts +12 -0
  87. package/template/server/src/routes/info.test.ts +20 -0
  88. package/template/server/src/routes/info.ts +19 -0
  89. package/template/server/src/shared.test.ts +53 -0
  90. package/template/server/src/shutdown.test.ts +98 -0
  91. package/template/server/src/socket.test.ts +185 -0
  92. package/template/server/src/static.test.ts +166 -0
  93. package/template/server/tsconfig.json +16 -0
  94. package/template/server/vitest.config.ts +22 -0
  95. package/template/shared/package.json +19 -0
  96. package/template/shared/src/constants.ts +11 -0
  97. package/template/shared/src/index.ts +8 -0
  98. package/template/shared/src/types.ts +33 -0
  99. package/template/shared/tsconfig.json +10 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # create-appystack
2
+
3
+ Scaffold a new [AppyStack](https://github.com/appydave/appystack) RVETS project in one command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-appystack my-app
9
+ ```
10
+
11
+ Or without a name (interactive):
12
+
13
+ ```bash
14
+ npx create-appystack
15
+ ```
16
+
17
+ The CLI will prompt for:
18
+
19
+ 1. **Project name** — directory name and package name suffix
20
+ 2. **Package scope** — npm scope (e.g. `@myorg`)
21
+ 3. **Server port** — default `5501`
22
+ 4. **Client port** — default `5500`
23
+ 5. **Description** — short project description
24
+
25
+ Then it copies the template, applies your settings, runs `npm install`, and prints next steps.
26
+
27
+ ## What You Get
28
+
29
+ A full-stack TypeScript monorepo with:
30
+
31
+ - **React 19 + Vite 7 + TailwindCSS v4** — client (your chosen port)
32
+ - **Express 5 + Socket.io + Pino + Zod** — server (your chosen port)
33
+ - **Shared TypeScript types** — workspace package
34
+ - **Vitest** — server + client tests
35
+ - **ESLint 9 flat config + Prettier** — via `@appydave/appystack-config`
36
+ - **Husky + lint-staged** — pre-commit hooks
37
+
38
+ ## After Creation
39
+
40
+ ```bash
41
+ cd my-app
42
+ npm run dev # Start client + server concurrently
43
+ npm test # Run all tests
44
+ npm run build # Production build
45
+ npm run typecheck # TypeScript check across all workspaces
46
+ ```
47
+
48
+ ## Stack
49
+
50
+ **R**eact · **V**ite · **E**xpress · **T**ypeScript · **S**ocket.io
51
+
52
+ ## License
53
+
54
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-appystack CLI
4
+ * Usage: npx create-appystack [project-name]
5
+ */
6
+ import { intro, outro, text, cancel, isCancel, spinner } from '@clack/prompts';
7
+ import { readFileSync, writeFileSync, cpSync, existsSync, rmSync } from 'node:fs';
8
+ import { resolve, dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { execSync } from 'node:child_process';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const TEMPLATE_DIR = resolve(__dirname, '../template');
14
+
15
+ function readFile(root, relPath) {
16
+ return readFileSync(resolve(root, relPath), 'utf-8');
17
+ }
18
+
19
+ function writeFile(root, relPath, content) {
20
+ writeFileSync(resolve(root, relPath), content, 'utf-8');
21
+ }
22
+
23
+ function replaceAll(content, from, to) {
24
+ return content.split(from).join(to);
25
+ }
26
+
27
+ const EXCLUDE = new Set(['node_modules', 'dist', 'coverage', 'test-results', '.git']);
28
+
29
+ function templateFilter(src) {
30
+ const segment = src.split('/').pop();
31
+ return !EXCLUDE.has(segment);
32
+ }
33
+
34
+ function applyCustomizations(root, { name, scope, serverPort, clientPort, desc }) {
35
+ const oldScope = '@appystack-template';
36
+ const oldRootName = '@appydave/appystack-template';
37
+ const oldServerPort = '5501';
38
+ const oldClientPort = '5500';
39
+ const oldTitle = 'AppyStack Template';
40
+ const oldDescription = 'RVETS stack boilerplate (React, Vite, Express, TypeScript, Socket.io)';
41
+
42
+ // Root package.json
43
+ let rootPkg = readFile(root, 'package.json');
44
+ rootPkg = replaceAll(rootPkg, oldRootName, `${scope}/${name}`);
45
+ rootPkg = replaceAll(rootPkg, oldDescription, desc);
46
+ writeFile(root, 'package.json', rootPkg);
47
+
48
+ // shared/package.json
49
+ let sharedPkg = readFile(root, 'shared/package.json');
50
+ sharedPkg = replaceAll(sharedPkg, oldScope, scope);
51
+ writeFile(root, 'shared/package.json', sharedPkg);
52
+
53
+ // server/package.json
54
+ let serverPkg = readFile(root, 'server/package.json');
55
+ serverPkg = replaceAll(serverPkg, oldScope, scope);
56
+ writeFile(root, 'server/package.json', serverPkg);
57
+
58
+ // client/package.json
59
+ let clientPkg = readFile(root, 'client/package.json');
60
+ clientPkg = replaceAll(clientPkg, oldScope, scope);
61
+ writeFile(root, 'client/package.json', clientPkg);
62
+
63
+ // .env.example
64
+ let envExample = readFile(root, '.env.example');
65
+ envExample = replaceAll(envExample, `PORT=${oldServerPort}`, `PORT=${serverPort}`);
66
+ envExample = replaceAll(
67
+ envExample,
68
+ `CLIENT_URL=http://localhost:${oldClientPort}`,
69
+ `CLIENT_URL=http://localhost:${clientPort}`
70
+ );
71
+ writeFile(root, '.env.example', envExample);
72
+
73
+ // server/src/config/env.ts
74
+ let envTs = readFile(root, 'server/src/config/env.ts');
75
+ envTs = replaceAll(
76
+ envTs,
77
+ `PORT: z.coerce.number().default(${oldServerPort})`,
78
+ `PORT: z.coerce.number().default(${serverPort})`
79
+ );
80
+ envTs = replaceAll(
81
+ envTs,
82
+ `CLIENT_URL: z.string().default('http://localhost:${oldClientPort}')`,
83
+ `CLIENT_URL: z.string().default('http://localhost:${clientPort}')`
84
+ );
85
+ writeFile(root, 'server/src/config/env.ts', envTs);
86
+
87
+ // client/vite.config.ts
88
+ let viteConfig = readFile(root, 'client/vite.config.ts');
89
+ viteConfig = replaceAll(viteConfig, `port: ${oldClientPort}`, `port: ${clientPort}`);
90
+ viteConfig = replaceAll(
91
+ viteConfig,
92
+ `target: 'http://localhost:${oldServerPort}'`,
93
+ `target: 'http://localhost:${serverPort}'`
94
+ );
95
+ writeFile(root, 'client/vite.config.ts', viteConfig);
96
+
97
+ // client/index.html
98
+ let indexHtml = readFile(root, 'client/index.html');
99
+ indexHtml = replaceAll(indexHtml, `<title>${oldTitle}</title>`, `<title>${name}</title>`);
100
+ writeFile(root, 'client/index.html', indexHtml);
101
+ }
102
+
103
+ async function main() {
104
+ const argName = process.argv[2];
105
+
106
+ intro('create-appystack');
107
+
108
+ // --- Project name ---
109
+ let projectName;
110
+ if (argName) {
111
+ if (!/^[a-z0-9-]+$/.test(argName.trim())) {
112
+ console.error('Error: project name must use lowercase letters, numbers, and hyphens only');
113
+ process.exit(1);
114
+ }
115
+ projectName = argName.trim();
116
+ } else {
117
+ const result = await text({
118
+ message: 'Project name (e.g. my-app)',
119
+ placeholder: 'my-app',
120
+ validate(value) {
121
+ if (!value || value.trim().length === 0) return 'Project name is required';
122
+ if (!/^[a-z0-9-]+$/.test(value.trim()))
123
+ return 'Use lowercase letters, numbers, and hyphens only';
124
+ },
125
+ });
126
+ if (isCancel(result)) { cancel('Cancelled.'); process.exit(0); }
127
+ projectName = result.trim();
128
+ }
129
+
130
+ // --- Check target doesn't exist ---
131
+ const targetDir = resolve(process.cwd(), projectName);
132
+ if (existsSync(targetDir)) {
133
+ console.error(`Error: directory "${projectName}" already exists`);
134
+ process.exit(1);
135
+ }
136
+
137
+ // --- Package scope ---
138
+ const scopeResult = await text({
139
+ message: 'Package scope (e.g. @myorg)',
140
+ placeholder: '@myorg',
141
+ validate(value) {
142
+ if (!value || value.trim().length === 0) return 'Package scope is required';
143
+ if (!value.trim().startsWith('@')) return 'Scope must start with @';
144
+ if (!/^@[a-z0-9-]+$/.test(value.trim())) return 'Use @lowercase-letters-numbers-hyphens only';
145
+ },
146
+ });
147
+ if (isCancel(scopeResult)) { cancel('Cancelled.'); process.exit(0); }
148
+ const packageScope = scopeResult.trim();
149
+
150
+ // --- Server port ---
151
+ const serverPortResult = await text({
152
+ message: 'Server port',
153
+ placeholder: '5501',
154
+ initialValue: '5501',
155
+ validate(value) {
156
+ const port = Number(value);
157
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
158
+ return 'Enter a valid port number (1–65535)';
159
+ },
160
+ });
161
+ if (isCancel(serverPortResult)) { cancel('Cancelled.'); process.exit(0); }
162
+ const serverPort = serverPortResult.trim();
163
+
164
+ // --- Client port ---
165
+ const clientPortResult = await text({
166
+ message: 'Client port',
167
+ placeholder: '5500',
168
+ initialValue: '5500',
169
+ validate(value) {
170
+ const port = Number(value);
171
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
172
+ return 'Enter a valid port number (1–65535)';
173
+ },
174
+ });
175
+ if (isCancel(clientPortResult)) { cancel('Cancelled.'); process.exit(0); }
176
+ const clientPort = clientPortResult.trim();
177
+
178
+ // --- Description ---
179
+ const descResult = await text({
180
+ message: 'Project description',
181
+ placeholder: 'My awesome app',
182
+ validate(value) {
183
+ if (!value || value.trim().length === 0) return 'Description is required';
184
+ },
185
+ });
186
+ if (isCancel(descResult)) { cancel('Cancelled.'); process.exit(0); }
187
+ const description = descResult.trim();
188
+
189
+ // --- Copy template ---
190
+ const s = spinner();
191
+ s.start('Copying template...');
192
+ try {
193
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true, filter: templateFilter });
194
+ s.stop('Template copied');
195
+ } catch (err) {
196
+ s.stop('Failed to copy template');
197
+ console.error(err.message);
198
+ rmSync(targetDir, { recursive: true, force: true });
199
+ process.exit(1);
200
+ }
201
+
202
+ // --- Apply customizations ---
203
+ s.start('Customizing project...');
204
+ try {
205
+ applyCustomizations(targetDir, {
206
+ name: projectName,
207
+ scope: packageScope,
208
+ serverPort,
209
+ clientPort,
210
+ desc: description,
211
+ });
212
+ s.stop('Project customized');
213
+ } catch (err) {
214
+ s.stop('Customization failed');
215
+ console.error(err.message);
216
+ rmSync(targetDir, { recursive: true, force: true });
217
+ process.exit(1);
218
+ }
219
+
220
+ // --- npm install ---
221
+ s.start('Installing dependencies (this may take a minute)...');
222
+ try {
223
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
224
+ s.stop('Dependencies installed');
225
+ } catch (err) {
226
+ s.stop('npm install failed');
227
+ console.error('Run "npm install" manually after cd into the project.');
228
+ }
229
+
230
+ outro(`Created ${projectName}
231
+
232
+ Next steps:
233
+ cd ${projectName}
234
+ npm run dev
235
+
236
+ Client: http://localhost:${clientPort}
237
+ Server: http://localhost:${serverPort}`);
238
+ }
239
+
240
+ main().catch((err) => {
241
+ console.error(err);
242
+ process.exit(1);
243
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "create-appystack",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new AppyStack RVETS project — React, Vite, Express, TypeScript, Socket.io",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-appystack": "bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "template/"
12
+ ],
13
+ "scripts": {
14
+ "sync": "node scripts/sync-template.js",
15
+ "prepublishOnly": "node scripts/sync-template.js"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^1.0.1"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "author": "David Cruwys",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/appydave/appystack.git"
28
+ },
29
+ "keywords": [
30
+ "appystack",
31
+ "create",
32
+ "scaffold",
33
+ "react",
34
+ "vite",
35
+ "express",
36
+ "typescript",
37
+ "socket.io"
38
+ ]
39
+ }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: recipe
3
+ description: "App architecture recipes for AppyStack projects. Use when a developer wants to build a specific type of application on top of the RVETS template — e.g. 'What recipes are available?', 'I want to build a CRUD app', 'I want a sidebar navigation layout', 'help me set up a nav-shell app', 'build me a file-based entity system', 'what can I build with AppyStack?', 'scaffold an app for me', 'I want a nav + data app'. Presents available recipes, generates a concrete build prompt for the chosen recipe, and asks for confirmation before building. Recipes can be used alone or combined."
4
+ ---
5
+
6
+ # Recipe
7
+
8
+ ## What Are Recipes?
9
+
10
+ Recipes are app architecture patterns that sit on top of the AppyStack RVETS template. Each recipe defines a specific structural shape — layout, data strategy, Socket.IO usage — that Claude scaffolds into the project.
11
+
12
+ Recipes are:
13
+ - **Stack-aware**: They know AppyStack's folder structure, installed libraries, and conventions
14
+ - **Composable**: Multiple recipes can run together (e.g. nav-shell + file-crud = a complete CRUD app)
15
+ - **Idempotent**: Each recipe checks whether it's already been applied before making changes
16
+
17
+ ---
18
+
19
+ ## Available Recipes
20
+
21
+ | Recipe | What it builds |
22
+ |--------|----------------|
23
+ | `nav-shell` | Left-sidebar navigation shell with header, collapsible sidebar, main content area, and optional footer. Menus can switch dynamically when sub-tools are active. Domain-agnostic layout scaffold. |
24
+ | `file-crud` | JSON file-based persistence for one or more entities. Each record is a file named `{slug}-{id}.json`. Real-time Socket.io sync. No database required. Includes chokidar file watcher. |
25
+
26
+ **Combinations:**
27
+ - `nav-shell` + `file-crud` = complete CRUD app with sidebar nav and file persistence
28
+ - `nav-shell` alone = visual shell, fill in data later
29
+ - `file-crud` alone = data layer only, wire up your own UI
30
+
31
+ **Reference files:**
32
+ - `references/nav-shell.md` — full nav-shell recipe spec
33
+ - `references/file-crud.md` — full file-crud recipe spec
34
+
35
+ **Domain samples** (for file-crud — example domains to draw entity/field inspiration from):
36
+ - `domains/care-provider-operations.md` — residential care provider (6 entities: Company, Site, User, Participant, Incident, Moment)
37
+ - `domains/youtube-launch-optimizer.md` — YouTube content production (5 entities: Channel, Video, Script, ThumbnailVariant, LaunchTask)
38
+
39
+ ---
40
+
41
+ ## Flow
42
+
43
+ 1. **Identify** which recipe(s) fit. If intent is unclear, ask: "What kind of app are you building?" and present the table above.
44
+ 2. **For file-crud**: ask if a domain sample applies, or collect entity details directly.
45
+ 3. **Load** the relevant reference file(s). Load both if combining.
46
+ 4. **Generate** a concrete build prompt — specific file structure, component names, data shapes, event names — tailored to this project. Not generic, not boilerplate descriptions.
47
+ 5. **Present** the prompt: "Here's what I'll build: ..." Show the specifics.
48
+ 6. **Ask**: "Shall I go ahead?"
49
+ 7. **Build** on confirmation, following the patterns in the reference file(s).
50
+
51
+ ---
52
+
53
+ ## Combining Recipes
54
+
55
+ When running `nav-shell` + `file-crud` together, collect domain context before generating either build prompt:
56
+
57
+ 1. Ask: what entities does the app need? (names, namish fields, key fields, relationships)
58
+ 2. Ask: what views/tools does the app need? (these become nav items)
59
+ 3. Ask: which entity maps to which view?
60
+ 4. Then generate: shell build prompt (with real view names from step 2) + persistence build prompt (with real entities from step 1)
61
+
62
+ The shell recipe generates view stubs. The persistence recipe generates server handlers and the `useEntity` hook. The developer (or a follow-up step) wires the `useEntity` hook into the view stubs.
63
+
64
+ ---
65
+
66
+ ## Notes
67
+
68
+ - Keep the generated prompt grounded in what's already in the template. Don't introduce new dependencies unless the recipe explicitly calls for them.
69
+ - The generated prompt is a useful artifact — if the developer says "not quite", refine it before building rather than starting over.
70
+ - For file-crud, always clarify the "namish field" — what field is used to name the file? Usually `name`, but not always.
71
+ - For nav-shell, always clarify which items are primary vs secondary, and whether any view needs a context-aware menu.
@@ -0,0 +1,185 @@
1
+ # Domain Sample: Care Provider Operations
2
+
3
+ A residential disability support provider app. Manages the org hierarchy (companies → sites → workers), the people being supported (participants), and the two types of care records workers create during shifts — incidents and moments.
4
+
5
+ Grounded in Australian NDIS (National Disability Insurance Scheme) context, but the structure applies to any regulated residential care setting.
6
+
7
+ **Use with**: `file-crud` recipe. Optionally combine with `nav-shell` recipe.
8
+
9
+ ---
10
+
11
+ ## Entities
12
+
13
+ ### Company
14
+ The registered care provider organisation.
15
+
16
+ | Field | Type | Notes |
17
+ |-------|------|-------|
18
+ | `name` | string | **namish field** — organisation display name |
19
+ | `slug` | string | URL-safe short name, e.g. `sunrise-care` |
20
+ | `abn` | string | Australian Business Number |
21
+ | `registeredNdisProvider` | boolean | regulatory compliance flag |
22
+ | `status` | string | `'active'` / `'suspended'` / `'onboarding'` |
23
+ | `createdAt` | string | ISO 8601 timestamp |
24
+
25
+ Namish field: `name`
26
+ Example filename: `sunrise-care-group-sc4f2a.json`
27
+
28
+ ---
29
+
30
+ ### Site
31
+ A physical location (group home, day program, etc.) operated by a Company.
32
+
33
+ | Field | Type | Notes |
34
+ |-------|------|-------|
35
+ | `name` | string | **namish field** — location display name |
36
+ | `companyId` | string | FK → Company (6-char id) |
37
+ | `address` | string | street address |
38
+ | `suburb` | string | |
39
+ | `state` | string | e.g. `VIC`, `NSW`, `QLD` |
40
+ | `postcode` | string | |
41
+ | `status` | string | `'active'` / `'inactive'` |
42
+
43
+ Namish field: `name`
44
+ Example filename: `thornbury-house-th7k3m.json`
45
+ Relationship: `companyId` → Company
46
+
47
+ ---
48
+
49
+ ### User
50
+ A staff member (support worker, team leader, or admin) employed by a Company.
51
+
52
+ | Field | Type | Notes |
53
+ |-------|------|-------|
54
+ | `name` | string | **namish field** — full name |
55
+ | `email` | string | |
56
+ | `companyId` | string | FK → Company |
57
+ | `roles` | string[] | array — e.g. `['support-worker', 'team-leader']` |
58
+ | `status` | string | `'active'` / `'inactive'` / `'invited'` |
59
+ | `createdAt` | string | ISO 8601 timestamp |
60
+
61
+ Namish field: `name`
62
+ Example filename: `angela-brown-ab9p2x.json`
63
+ Relationship: `companyId` → Company
64
+
65
+ ---
66
+
67
+ ### Participant
68
+ A person receiving support under an NDIS plan, supported by a Company at a primary Site.
69
+
70
+ | Field | Type | Notes |
71
+ |-------|------|-------|
72
+ | `firstName` | string | |
73
+ | `lastName` | string | **namish field** (composite `firstName-lastName`) |
74
+ | `preferredName` | string \| null | optional — if different from first name |
75
+ | `ndisNumber` | string | NDIS plan number, e.g. `512384901` |
76
+ | `dateOfBirth` | string | ISO 8601 date |
77
+ | `companyId` | string | FK → Company |
78
+ | `defaultSiteId` | string | FK → Site (primary home) |
79
+ | `baselineDataTier` | number | NDIS funding tier: `1` / `2` / `3` / `4` |
80
+ | `status` | string | `'active'` / `'inactive'` / `'transitioned'` |
81
+
82
+ Namish field: composite `${firstName}-${lastName}`
83
+ Example filename: `rosie-fairweather-rf3n8k.json`
84
+ Relationships: `companyId` → Company, `defaultSiteId` → Site
85
+
86
+ ---
87
+
88
+ ### Incident
89
+ A significant event at a site involving a participant. Requires formal recording and may require regulatory reporting.
90
+
91
+ | Field | Type | Notes |
92
+ |-------|------|-------|
93
+ | `summary` | string | **namish field** — brief description of what happened |
94
+ | `type` | string | `'behavioural'` / `'medical'` / `'environmental'` / `'other'` |
95
+ | `severity` | string | `'low'` / `'medium'` / `'high'` / `'critical'` |
96
+ | `status` | string | `'draft'` / `'submitted'` / `'under-review'` / `'closed'` |
97
+ | `occurredAt` | string | ISO 8601 timestamp of the event |
98
+ | `antecedents` | string[] | triggering factors, e.g. `['routine change', 'unfamiliar worker']` |
99
+ | `companyId` | string | FK → Company |
100
+ | `participantId` | string | FK → Participant (who was involved) |
101
+ | `siteId` | string | FK → Site (where it happened) |
102
+ | `reportedById` | string | FK → User (who filed it) |
103
+ | `createdAt` | string | ISO 8601 timestamp — when it was logged |
104
+
105
+ Namish field: `summary`
106
+ Example filename: `distressed-during-morning-routine-inc-k4p9m.json`
107
+ Relationships: `companyId` → Company, `participantId` → Participant, `siteId` → Site, `reportedById` → User
108
+
109
+ ---
110
+
111
+ ### Moment
112
+ A routine care observation recorded by a worker during a shift. Lower-stakes than an incident — used to build a picture of a participant's daily wellbeing over time.
113
+
114
+ | Field | Type | Notes |
115
+ |-------|------|-------|
116
+ | `note` | string | **namish field** — the observation, e.g. "Tommy helped set the table today" |
117
+ | `category` | string | `'positive'` / `'concerning'` / `'neutral'` |
118
+ | `occurredAt` | string | ISO 8601 timestamp |
119
+ | `companyId` | string | FK → Company |
120
+ | `participantId` | string | FK → Participant (who this is about) |
121
+ | `siteId` | string | FK → Site |
122
+ | `reportedById` | string | FK → User (observer) |
123
+ | `createdAt` | string | ISO 8601 timestamp — when it was logged |
124
+
125
+ Namish field: `note` (truncated to slug-safe length)
126
+ Example filename: `helped-set-the-table-mom-r7n2x.json`
127
+ Relationships: `companyId` → Company, `participantId` → Participant, `siteId` → Site, `reportedById` → User
128
+
129
+ ---
130
+
131
+ ## Entity Classification
132
+
133
+ | Entity | Type | Notes |
134
+ |--------|------|-------|
135
+ | Company | System / configuration | Set up once, rarely changes |
136
+ | Site | System / configuration | Set up once per location |
137
+ | User | System / configuration | Managed by admin |
138
+ | Participant | System / configuration | Registered on intake, updated periodically |
139
+ | Incident | Domain / operational | Created when significant events occur |
140
+ | Moment | Domain / operational | Created every shift — high volume |
141
+
142
+ ---
143
+
144
+ ## Suggested Nav Mapping (for nav-shell recipe)
145
+
146
+ | Nav Item | View Key | Entity | Tier |
147
+ |----------|----------|--------|------|
148
+ | Dashboard | `dashboard` | — (summary counts + recent activity) | primary |
149
+ | Participants | `participants` | Participant | primary |
150
+ | Incidents | `incidents` | Incident | primary |
151
+ | Moments | `moments` | Moment | primary |
152
+ | Sites | `sites` | Site | secondary |
153
+ | Users | `users` | User | secondary |
154
+ | Companies | `companies` | Company | secondary |
155
+
156
+ ---
157
+
158
+ ## Data Folder Structure
159
+
160
+ ```
161
+ data/
162
+ ├── companies/
163
+ │ └── sunrise-care-group-sc4f2a.json
164
+ ├── sites/
165
+ │ └── thornbury-house-th7k3m.json
166
+ ├── users/
167
+ │ └── angela-brown-ab9p2x.json
168
+ ├── participants/
169
+ │ └── rosie-fairweather-rf3n8k.json
170
+ ├── incidents/
171
+ │ └── distressed-during-morning-routine-inc-k4p9m.json
172
+ └── moments/
173
+ └── helped-set-the-table-mom-r7n2x.json
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Notes
179
+
180
+ - **Incidents vs Moments**: Both attach to a participant + site + worker. Incidents are formal reportable events with severity and workflow status. Moments are routine shift observations — high-frequency, qualitative, no escalation path.
181
+ - **Participant is the central entity**: Sites, Incidents, and Moments all link to a Participant. When viewing a participant's record, show their site, recent moments, and incident history together.
182
+ - **Company scoping**: Every entity except Company has a `companyId`. In a multi-company setup, always filter by `companyId` to avoid data leakage between orgs.
183
+ - **User roles are an array**: A team leader may also be a support worker. Don't assume a single role per user.
184
+ - **Participant `preferredName`**: Always check and display preferred name if set — this matters for person-centred care.
185
+ - **`baselineDataTier`** drives how much observation data is expected per participant. Tier 3-4 participants require more frequent Moments and stricter Incident review.