spine-framework 0.1.61 → 0.2.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/.framework/cli/commands/create-app.ts +1 -1
- package/.framework/cli/commands/dev.ts +215 -33
- package/.framework/cli/index.ts +37 -63
- package/.framework/functions/_shared/app-manifest.ts +8 -15
- package/.framework/functions/_shared/db.ts +11 -15
- package/.framework/functions/_shared/index.ts +0 -9
- package/.framework/functions/_shared/principal.ts +4 -62
- package/.framework/functions/_shared/resolve-ids.ts +6 -6
- package/.framework/functions/_shared/webhook-registry.ts +1 -1
- package/.framework/functions/admin-funnel.ts +104 -0
- package/.framework/functions/admin-install-funnel.ts +225 -0
- package/.framework/functions/admin-signals.ts +241 -0
- package/.framework/migrations/001_schema.sql +406 -0
- package/.framework/migrations/002_rls_and_functions.sql +710 -0
- package/.framework/migrations/003_system_data.sql +520 -0
- package/.framework/migrations/004_enhancements.sql +43 -0
- package/.framework/public/_redirects +3 -0
- package/.framework/public/spine-logo.jpg +0 -0
- package/.framework/public/spine-logo.svg +3 -0
- package/{scripts → .framework/scripts}/assemble-frontend.sh +1 -1
- package/{scripts → .framework/scripts}/assemble-functions.sh +1 -1
- package/{scripts → .framework/scripts}/assemble.sh +2 -8
- package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
- package/.framework/seeds/link_types/account_cli_instances.json +9 -0
- package/.framework/seeds/types/funnel_signal_use.json +98 -0
- package/.framework/seeds/types/spine_instance.json +109 -0
- package/.framework/src/App.tsx +6 -4
- package/.framework/src/components/ui/accordion.tsx +1 -1
- package/.framework/src/components/ui/alert-dialog.tsx +1 -1
- package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
- package/.framework/src/components/ui/avatar.tsx +1 -1
- package/.framework/src/components/ui/badge.tsx +2 -2
- package/.framework/src/components/ui/breadcrumb.tsx +2 -2
- package/.framework/src/components/ui/button-group.tsx +2 -2
- package/.framework/src/components/ui/button.tsx +2 -2
- package/.framework/src/components/ui/chart.tsx +6 -1
- package/.framework/src/components/ui/checkbox.tsx +1 -1
- package/.framework/src/components/ui/collapsible.tsx +1 -1
- package/.framework/src/components/ui/context-menu.tsx +1 -1
- package/.framework/src/components/ui/dialog.tsx +1 -1
- package/.framework/src/components/ui/direction.tsx +17 -1
- package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
- package/.framework/src/components/ui/hover-card.tsx +1 -1
- package/.framework/src/components/ui/item.tsx +2 -2
- package/.framework/src/components/ui/label.tsx +1 -1
- package/.framework/src/components/ui/menubar.tsx +1 -1
- package/.framework/src/components/ui/navigation-menu.tsx +1 -1
- package/.framework/src/components/ui/popover.tsx +1 -1
- package/.framework/src/components/ui/progress.tsx +1 -1
- package/.framework/src/components/ui/radio-group.tsx +1 -1
- package/.framework/src/components/ui/scroll-area.tsx +1 -1
- package/.framework/src/components/ui/select.tsx +1 -1
- package/.framework/src/components/ui/separator.tsx +1 -1
- package/.framework/src/components/ui/sheet.tsx +1 -1
- package/.framework/src/components/ui/sidebar.tsx +6 -6
- package/.framework/src/components/ui/slider.tsx +1 -1
- package/.framework/src/components/ui/switch.tsx +1 -1
- package/.framework/src/components/ui/tabs.tsx +1 -1
- package/.framework/src/components/ui/toggle-group.tsx +1 -1
- package/.framework/src/components/ui/toggle.tsx +1 -1
- package/.framework/src/components/ui/tooltip.tsx +1 -1
- package/.framework/src/pages/auth/LoginPage.tsx +10 -9
- package/LICENSE.md +193 -0
- package/README.md +254 -0
- package/custom/.devin/skills/install-app/SKILL.md +39 -0
- package/custom/.devin/workflows/app-install.yaml +83 -0
- package/custom/README.md +79 -0
- package/docs/APPS.md +232 -0
- package/docs/ARCHITECTURE.md +246 -0
- package/docs/INSTALL.md +155 -0
- package/docs/README.md +170 -0
- package/docs/design-schema-spec.md +1837 -0
- package/docs/dev-guides/01-create-an-app.md +183 -0
- package/docs/dev-guides/02-create-a-function.md +178 -0
- package/docs/dev-guides/03-create-a-component.md +178 -0
- package/docs/dev-guides/04-webhook-handlers.md +129 -0
- package/docs/dev-guides/05-testing.md +168 -0
- package/docs/dev-guides/README.md +40 -0
- package/docs/npm-packages-plan.md +288 -0
- package/docs/spine-core--api-reference.md +822 -0
- package/package.json +78 -144
- package/.framework/cli/bin.cjs +0 -14
- package/.framework/cli/commands/init.ts +0 -186
- package/.framework/cli/commands/install-app.ts +0 -565
- package/.framework/cli/commands/migrate.ts +0 -139
- package/.framework/cli/commands/status.ts +0 -197
- package/.framework/cli/commands/uninstall-app.ts +0 -166
- package/.framework/cli/welcome.cjs +0 -45
- package/.framework/docs/STABILITY.md +0 -52
- package/.framework/docs/examples/custom-app/README.md +0 -77
- package/.framework/docs/examples/custom-function/README.md +0 -27
- package/.framework/docs/examples/custom-function/handler.ts +0 -48
- package/.framework/docs/examples/custom-webhook/README.md +0 -68
- package/.framework/migrations/000_foundation.sql +0 -1266
- package/.framework/migrations/001_seed.sql +0 -163
- package/.framework/migrations/002_seed_constraints.sql +0 -19
- package/.framework/migrations/003_auth_user_trigger.sql +0 -67
- package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
- package/STRUCTURE.md +0 -150
- package/bin/spine-framework.cjs +0 -62
- package/bin/welcome.cjs +0 -45
- package/bin/ws-shim.cjs +0 -8
- package/bin/ws-shim.ts +0 -10
- package/config/components.json +0 -25
- package/config/deno.lock +0 -108
- package/config/package-lock.json +0 -17183
- package/config/postcss.config.cjs +0 -10
- package/config/tailwind.config.cjs +0 -78
- package/config/tsconfig.build.json +0 -32
- package/config/tsconfig.cli.json +0 -18
- package/config/tsconfig.json +0 -41
- package/config/tsconfig.node.json +0 -17
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/typedoc.json +0 -16
- package/config/vite.config.d.ts +0 -2
- package/config/vite.config.ts +0 -72
- package/dist/cli/commands/agents.d.ts +0 -39
- package/dist/cli/commands/agents.d.ts.map +0 -1
- package/dist/cli/commands/auth.d.ts +0 -36
- package/dist/cli/commands/auth.d.ts.map +0 -1
- package/dist/cli/commands/create-app.d.ts +0 -23
- package/dist/cli/commands/create-app.d.ts.map +0 -1
- package/dist/cli/commands/dev.d.ts +0 -24
- package/dist/cli/commands/dev.d.ts.map +0 -1
- package/dist/cli/commands/doctor.d.ts +0 -42
- package/dist/cli/commands/doctor.d.ts.map +0 -1
- package/dist/cli/commands/generate.d.ts +0 -36
- package/dist/cli/commands/generate.d.ts.map +0 -1
- package/dist/cli/commands/init.d.ts +0 -20
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/install-app.d.ts +0 -30
- package/dist/cli/commands/install-app.d.ts.map +0 -1
- package/dist/cli/commands/items.d.ts +0 -45
- package/dist/cli/commands/items.d.ts.map +0 -1
- package/dist/cli/commands/migrate.d.ts +0 -21
- package/dist/cli/commands/migrate.d.ts.map +0 -1
- package/dist/cli/commands/migrations.d.ts +0 -41
- package/dist/cli/commands/migrations.d.ts.map +0 -1
- package/dist/cli/commands/pipelines.d.ts +0 -40
- package/dist/cli/commands/pipelines.d.ts.map +0 -1
- package/dist/cli/commands/status.d.ts +0 -23
- package/dist/cli/commands/status.d.ts.map +0 -1
- package/dist/cli/commands/system.d.ts +0 -29
- package/dist/cli/commands/system.d.ts.map +0 -1
- package/dist/cli/commands/test.d.ts +0 -46
- package/dist/cli/commands/test.d.ts.map +0 -1
- package/dist/cli/commands/uninstall-app.d.ts +0 -23
- package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
- package/dist/cli/context.d.ts +0 -88
- package/dist/cli/context.d.ts.map +0 -1
- package/dist/cli/env-loader.d.ts +0 -14
- package/dist/cli/env-loader.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -41
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/functions/_shared/agent-runner.d.ts +0 -156
- package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
- package/dist/functions/_shared/app-manifest.d.ts +0 -68
- package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
- package/dist/functions/_shared/audit.d.ts +0 -91
- package/dist/functions/_shared/audit.d.ts.map +0 -1
- package/dist/functions/_shared/db.d.ts +0 -125
- package/dist/functions/_shared/db.d.ts.map +0 -1
- package/dist/functions/_shared/index.d.ts +0 -299
- package/dist/functions/_shared/index.d.ts.map +0 -1
- package/dist/functions/_shared/middleware.d.ts +0 -315
- package/dist/functions/_shared/middleware.d.ts.map +0 -1
- package/dist/functions/_shared/permissions.d.ts +0 -626
- package/dist/functions/_shared/permissions.d.ts.map +0 -1
- package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
- package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
- package/dist/functions/_shared/principal.d.ts +0 -284
- package/dist/functions/_shared/principal.d.ts.map +0 -1
- package/dist/functions/_shared/resolve-ids.d.ts +0 -10
- package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
- package/dist/functions/_shared/schema-utils.d.ts +0 -181
- package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
- package/dist/functions/_shared/testing.d.ts +0 -172
- package/dist/functions/_shared/testing.d.ts.map +0 -1
- package/dist/functions/_shared/trigger-engine.d.ts +0 -140
- package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registration.d.ts +0 -81
- package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registry.d.ts +0 -57
- package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
- package/dist/functions/account-nodes.d.ts +0 -48
- package/dist/functions/account-nodes.d.ts.map +0 -1
- package/dist/functions/admin-data.d.ts +0 -178
- package/dist/functions/admin-data.d.ts.map +0 -1
- package/dist/functions/ai-agents.d.ts +0 -125
- package/dist/functions/ai-agents.d.ts.map +0 -1
- package/dist/functions/api-keys.d.ts +0 -140
- package/dist/functions/api-keys.d.ts.map +0 -1
- package/dist/functions/apps.d.ts +0 -163
- package/dist/functions/apps.d.ts.map +0 -1
- package/dist/functions/auth.d.ts +0 -74
- package/dist/functions/auth.d.ts.map +0 -1
- package/dist/functions/debug-auth.d.ts +0 -33
- package/dist/functions/debug-auth.d.ts.map +0 -1
- package/dist/functions/embeddings.d.ts +0 -205
- package/dist/functions/embeddings.d.ts.map +0 -1
- package/dist/functions/integration-routes.d.ts +0 -45
- package/dist/functions/integration-routes.d.ts.map +0 -1
- package/dist/functions/integrations.d.ts +0 -124
- package/dist/functions/integrations.d.ts.map +0 -1
- package/dist/functions/item-progress.d.ts +0 -41
- package/dist/functions/item-progress.d.ts.map +0 -1
- package/dist/functions/logs.d.ts +0 -162
- package/dist/functions/logs.d.ts.map +0 -1
- package/dist/functions/observability.d.ts +0 -123
- package/dist/functions/observability.d.ts.map +0 -1
- package/dist/functions/pipeline-executions.d.ts +0 -190
- package/dist/functions/pipeline-executions.d.ts.map +0 -1
- package/dist/functions/pipelines.d.ts +0 -171
- package/dist/functions/pipelines.d.ts.map +0 -1
- package/dist/functions/prompt-configs.d.ts +0 -125
- package/dist/functions/prompt-configs.d.ts.map +0 -1
- package/dist/functions/roles.d.ts +0 -118
- package/dist/functions/roles.d.ts.map +0 -1
- package/dist/functions/system-cron.d.ts +0 -65
- package/dist/functions/system-cron.d.ts.map +0 -1
- package/dist/functions/system.d.ts +0 -29
- package/dist/functions/system.d.ts.map +0 -1
- package/dist/functions/tests.d.ts +0 -28
- package/dist/functions/tests.d.ts.map +0 -1
- package/dist/functions/timers.d.ts +0 -139
- package/dist/functions/timers.d.ts.map +0 -1
- package/dist/functions/triggers.d.ts +0 -203
- package/dist/functions/triggers.d.ts.map +0 -1
- package/dist/functions/types.d.ts +0 -151
- package/dist/functions/types.d.ts.map +0 -1
- package/dist/src/types/types.d.ts +0 -364
- package/dist/src/types/types.d.ts.map +0 -1
- package/index.html +0 -13
- package/netlify.toml +0 -36
- package/package-project.json +0 -64
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/netlify-dev-wrapper.sh +0 -22
- package/vitest.config.ts +0 -45
- /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
- /package/{scripts → .framework/scripts}/boundary-check.sh +0 -0
- /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
- /package/{scripts → .framework/scripts}/verify-integrity.sh +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Guide 01: Create a Custom App
|
|
2
|
+
|
|
3
|
+
## What this guide covers
|
|
4
|
+
|
|
5
|
+
How to scaffold, configure, and run a new custom app on Spine.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Step 1: Scaffold with the CLI
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run spine-framework create-app my-app -- --name "My App" --role member
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This creates:
|
|
16
|
+
```
|
|
17
|
+
custom/apps/my-app/
|
|
18
|
+
manifest.json ← app metadata, routes, nav, roles
|
|
19
|
+
index.tsx ← React entry point
|
|
20
|
+
components/ ← your components go here
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And inserts a row into the `apps` table in the database.
|
|
24
|
+
|
|
25
|
+
**Options:**
|
|
26
|
+
```bash
|
|
27
|
+
--name "My App" # Display name (defaults to title-cased slug)
|
|
28
|
+
--role member # Required role: member | support | system_admin (default: member)
|
|
29
|
+
--force # Overwrite if directory already exists
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 2: Configure manifest.json
|
|
35
|
+
|
|
36
|
+
`custom/apps/my-app/manifest.json` controls everything about the app.
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"name": "My App",
|
|
41
|
+
"slug": "my-app",
|
|
42
|
+
"description": "What this app does",
|
|
43
|
+
"version": "1.0.0",
|
|
44
|
+
"required_roles": ["member"],
|
|
45
|
+
"routes": [
|
|
46
|
+
"/my-app",
|
|
47
|
+
"/my-app/dashboard",
|
|
48
|
+
"/my-app/settings"
|
|
49
|
+
],
|
|
50
|
+
"nav_items": [
|
|
51
|
+
{
|
|
52
|
+
"title": "Dashboard",
|
|
53
|
+
"path": "/my-app/dashboard",
|
|
54
|
+
"icon": "LayoutDashboard",
|
|
55
|
+
"order": 1
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"title": "Settings",
|
|
59
|
+
"path": "/my-app/settings",
|
|
60
|
+
"icon": "Settings",
|
|
61
|
+
"order": 2
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"features": [],
|
|
65
|
+
"entry_point": "./index.tsx"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Key fields:**
|
|
70
|
+
- `required_roles` — array of roles that can access this app. Any match grants access.
|
|
71
|
+
- `routes` — informational; actual routing is handled by `index.tsx`
|
|
72
|
+
- `nav_items` — used by generic app shells; custom apps render their own sidebar
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Step 3: Build index.tsx
|
|
77
|
+
|
|
78
|
+
`index.tsx` is the React entry point. Core mounts it at `/{slug}/*`.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import * as React from 'react'
|
|
82
|
+
import { Routes, Route, Navigate } from 'react-router-dom'
|
|
83
|
+
import { AppShell } from '@core/components/layout/AppShell'
|
|
84
|
+
import { useAuth } from '@core/contexts/AuthContext'
|
|
85
|
+
import MyAppSidebar from './components/MyAppSidebar'
|
|
86
|
+
|
|
87
|
+
function Dashboard() {
|
|
88
|
+
const { user } = useAuth()
|
|
89
|
+
return (
|
|
90
|
+
<div className="p-6">
|
|
91
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
92
|
+
<p className="text-slate-500">Welcome, {user?.full_name}</p>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function MyApp() {
|
|
98
|
+
return (
|
|
99
|
+
<AppShell sidebar={<MyAppSidebar />}>
|
|
100
|
+
<Routes>
|
|
101
|
+
<Route index element={<Navigate to="dashboard" replace />} />
|
|
102
|
+
<Route path="dashboard" element={<Dashboard />} />
|
|
103
|
+
</Routes>
|
|
104
|
+
</AppShell>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Rules for index.tsx:**
|
|
110
|
+
- Always `export default` the root component
|
|
111
|
+
- Use `@core/...` imports for anything from core — never relative paths to `.framework/`
|
|
112
|
+
- Routes are relative — `/my-app/dashboard` is just `path="dashboard"` here
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Step 4: Assemble and run
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run assemble
|
|
120
|
+
netlify dev
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Navigate to `http://localhost:8888/my-app`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Step 5: Verify
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# App files exist
|
|
131
|
+
ls custom/apps/my-app/
|
|
132
|
+
|
|
133
|
+
# App is in the database
|
|
134
|
+
# Check: GET /api/apps?action=list should include your slug
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## File structure reference
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
custom/apps/my-app/
|
|
143
|
+
manifest.json ← required
|
|
144
|
+
index.tsx ← required: default export = root component
|
|
145
|
+
components/
|
|
146
|
+
MyAppSidebar.tsx ← optional: custom sidebar
|
|
147
|
+
MyFeatureCard.tsx ← optional: any component
|
|
148
|
+
pages/
|
|
149
|
+
DashboardPage.tsx ← optional: split pages out for readability
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Core imports available in custom apps
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// Auth
|
|
158
|
+
import { useAuth } from '@core/contexts/AuthContext'
|
|
159
|
+
|
|
160
|
+
// Layout
|
|
161
|
+
import { AppShell } from '@core/components/layout/AppShell'
|
|
162
|
+
|
|
163
|
+
// UI primitives
|
|
164
|
+
import { Button } from '@core/components/ui/button'
|
|
165
|
+
import { LoadingSpinner } from '@core/components/ui/LoadingSpinner'
|
|
166
|
+
import { Sidebar, SidebarMenu, SidebarMenuItem } from '@core/components/ui/sidebar'
|
|
167
|
+
|
|
168
|
+
// Data fetching
|
|
169
|
+
import { apiFetch } from '@core/lib/api'
|
|
170
|
+
|
|
171
|
+
// Router (re-exported from react-router-dom)
|
|
172
|
+
import { Routes, Route, Navigate, useNavigate, useParams } from 'react-router-dom'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> **Rule:** Always use `@core/...` for core imports. Never use relative paths like `../../../../.framework/src/...`.
|
|
176
|
+
> `@core` resolves to `.assembled/src` in both dev and build.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Next steps
|
|
181
|
+
|
|
182
|
+
- [02-create-a-function.md](./02-create-a-function.md) — Add a backend API to your app
|
|
183
|
+
- [03-create-a-component.md](./03-create-a-component.md) — Build reusable components
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Guide 02: Create a Custom Function
|
|
2
|
+
|
|
3
|
+
## What this guide covers
|
|
4
|
+
|
|
5
|
+
How to write a custom Netlify function that uses core authentication, database access, and middleware.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Naming convention
|
|
10
|
+
|
|
11
|
+
Custom functions live in `custom/functions/` and must be prefixed `custom_`:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
custom/functions/custom_my-feature.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After assembly, this becomes `.netlify/functions/custom_my-feature` and is callable at:
|
|
18
|
+
```
|
|
19
|
+
GET /api/custom_my-feature?action=list
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 1: Create the function file
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// custom/functions/custom_my-feature.ts
|
|
28
|
+
import { createHandler } from '@core/_shared'
|
|
29
|
+
|
|
30
|
+
export const handler = createHandler(async (event, ctx) => {
|
|
31
|
+
const action = ctx.query?.action || 'list'
|
|
32
|
+
|
|
33
|
+
switch (action) {
|
|
34
|
+
case 'list': {
|
|
35
|
+
const { data, error } = await ctx.db
|
|
36
|
+
.from('items')
|
|
37
|
+
.select('*')
|
|
38
|
+
.eq('type_slug', 'my_type')
|
|
39
|
+
|
|
40
|
+
if (error) throw error
|
|
41
|
+
return { data }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'get': {
|
|
45
|
+
const id = ctx.query?.id
|
|
46
|
+
if (!id) throw new Error('id is required')
|
|
47
|
+
|
|
48
|
+
const { data, error } = await ctx.db
|
|
49
|
+
.from('items')
|
|
50
|
+
.select('*')
|
|
51
|
+
.eq('id', id)
|
|
52
|
+
.single()
|
|
53
|
+
|
|
54
|
+
if (error) throw error
|
|
55
|
+
return { data }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unknown action: ${action}`)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What createHandler gives you
|
|
67
|
+
|
|
68
|
+
`createHandler` wraps your function with:
|
|
69
|
+
- **Authentication** — rejects unauthenticated requests with 401
|
|
70
|
+
- **Authorization** — `ctx.principal` has the user's roles and permissions
|
|
71
|
+
- **Database** — `ctx.db` is a Supabase client scoped to the user's account via RLS
|
|
72
|
+
- **Logging** — `ctx.logger` for structured logs
|
|
73
|
+
- **Error handling** — uncaught errors become 500 responses with safe messages
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
interface CoreContext {
|
|
77
|
+
db: SupabaseClient // RLS-scoped to authenticated user's account
|
|
78
|
+
principal: Principal // { id, account_id, roles, permissions }
|
|
79
|
+
query: Record<string, string> // parsed query string
|
|
80
|
+
body: any // parsed request body
|
|
81
|
+
logger: Logger // structured logging
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Accessing the authenticated user
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
export const handler = createHandler(async (event, ctx) => {
|
|
91
|
+
const { principal } = ctx
|
|
92
|
+
|
|
93
|
+
console.log(principal.id) // user UUID
|
|
94
|
+
console.log(principal.account_id) // tenant account UUID
|
|
95
|
+
console.log(principal.roles) // e.g. ['member', 'support']
|
|
96
|
+
|
|
97
|
+
// Check role
|
|
98
|
+
if (!principal.roles.includes('support')) {
|
|
99
|
+
throw new Error('Support role required')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { data: 'ok' }
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Admin database access
|
|
109
|
+
|
|
110
|
+
`ctx.db` is RLS-scoped (safe for user-facing data). For admin operations use `adminDb`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { createHandler, adminDb } from '@core/_shared'
|
|
114
|
+
|
|
115
|
+
export const handler = createHandler(async (event, ctx) => {
|
|
116
|
+
// adminDb bypasses RLS — use with care
|
|
117
|
+
const { data } = await adminDb
|
|
118
|
+
.from('accounts')
|
|
119
|
+
.select('*')
|
|
120
|
+
|
|
121
|
+
return { data }
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Calling your function from the frontend
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { apiFetch } from '@core/lib/api'
|
|
131
|
+
|
|
132
|
+
// apiFetch handles auth headers and the /api → /.netlify/functions proxy
|
|
133
|
+
const response = await apiFetch('/api/custom_my-feature?action=list')
|
|
134
|
+
const { data } = await response.json()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Step 2: Assemble and test
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run assemble:functions
|
|
143
|
+
# Functions reload automatically in netlify dev
|
|
144
|
+
|
|
145
|
+
# Test directly
|
|
146
|
+
curl http://localhost:8888/api/custom_my-feature?action=list \
|
|
147
|
+
-H "Authorization: Bearer <your-jwt>"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## POST requests
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
export const handler = createHandler(async (event, ctx) => {
|
|
156
|
+
if (event.httpMethod === 'POST') {
|
|
157
|
+
const payload = ctx.body // already parsed JSON
|
|
158
|
+
|
|
159
|
+
const { data, error } = await ctx.db
|
|
160
|
+
.from('items')
|
|
161
|
+
.insert({ ...payload, account_id: ctx.principal.account_id })
|
|
162
|
+
.select()
|
|
163
|
+
.single()
|
|
164
|
+
|
|
165
|
+
if (error) throw error
|
|
166
|
+
return { data }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error('Method not allowed')
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Next steps
|
|
176
|
+
|
|
177
|
+
- [03-create-a-component.md](./03-create-a-component.md) — Build UI components for your app
|
|
178
|
+
- [04-webhook-handlers.md](./04-webhook-handlers.md) — React to system events
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Guide 03: Create a Custom Component
|
|
2
|
+
|
|
3
|
+
## What this guide covers
|
|
4
|
+
|
|
5
|
+
How to build React components inside a custom app, using core hooks, UI primitives, and layout components.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Where components live
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
custom/apps/my-app/
|
|
13
|
+
components/
|
|
14
|
+
MyAppSidebar.tsx ← navigation sidebar
|
|
15
|
+
MyFeatureCard.tsx ← any reusable UI piece
|
|
16
|
+
MyDataTable.tsx ← data display
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Components are local to your app. They are never imported by core.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Import rules
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// ✅ Correct — use @core alias for anything from core
|
|
27
|
+
import { useAuth } from '@core/contexts/AuthContext'
|
|
28
|
+
import { Button } from '@core/components/ui/button'
|
|
29
|
+
import { AppShell } from '@core/components/layout/AppShell'
|
|
30
|
+
|
|
31
|
+
// ✅ Correct — relative imports within your own app
|
|
32
|
+
import MyFeatureCard from './MyFeatureCard'
|
|
33
|
+
import MyFeatureCard from '../components/MyFeatureCard'
|
|
34
|
+
|
|
35
|
+
// ❌ Wrong — never use relative paths to .framework
|
|
36
|
+
import { useAuth } from '../../../../.framework/src/contexts/AuthContext'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Building a sidebar
|
|
42
|
+
|
|
43
|
+
Sidebars use the core `Sidebar` primitives from shadcn/ui.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// custom/apps/my-app/components/MyAppSidebar.tsx
|
|
47
|
+
import * as React from 'react'
|
|
48
|
+
import { useNavigate, useLocation } from 'react-router-dom'
|
|
49
|
+
import { useAuth } from '@core/contexts/AuthContext'
|
|
50
|
+
import {
|
|
51
|
+
Sidebar,
|
|
52
|
+
SidebarContent,
|
|
53
|
+
SidebarGroup,
|
|
54
|
+
SidebarGroupContent,
|
|
55
|
+
SidebarGroupLabel,
|
|
56
|
+
SidebarHeader,
|
|
57
|
+
SidebarMenu,
|
|
58
|
+
SidebarMenuButton,
|
|
59
|
+
SidebarMenuItem,
|
|
60
|
+
SidebarRail,
|
|
61
|
+
} from '@core/components/ui/sidebar'
|
|
62
|
+
import { LayoutDashboard, Settings } from 'lucide-react'
|
|
63
|
+
|
|
64
|
+
const navItems = [
|
|
65
|
+
{ title: 'Dashboard', url: '/my-app/dashboard', icon: LayoutDashboard },
|
|
66
|
+
{ title: 'Settings', url: '/my-app/settings', icon: Settings },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
export function MyAppSidebar() {
|
|
70
|
+
const navigate = useNavigate()
|
|
71
|
+
const location = useLocation()
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Sidebar>
|
|
75
|
+
<SidebarHeader>
|
|
76
|
+
<span className="font-semibold px-2">My App</span>
|
|
77
|
+
</SidebarHeader>
|
|
78
|
+
<SidebarContent>
|
|
79
|
+
<SidebarGroup>
|
|
80
|
+
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
81
|
+
<SidebarGroupContent>
|
|
82
|
+
<SidebarMenu>
|
|
83
|
+
{navItems.map(item => (
|
|
84
|
+
<SidebarMenuItem key={item.url}>
|
|
85
|
+
<SidebarMenuButton
|
|
86
|
+
isActive={location.pathname.startsWith(item.url)}
|
|
87
|
+
onClick={() => navigate(item.url)}
|
|
88
|
+
>
|
|
89
|
+
<item.icon className="w-4 h-4" />
|
|
90
|
+
<span>{item.title}</span>
|
|
91
|
+
</SidebarMenuButton>
|
|
92
|
+
</SidebarMenuItem>
|
|
93
|
+
))}
|
|
94
|
+
</SidebarMenu>
|
|
95
|
+
</SidebarGroupContent>
|
|
96
|
+
</SidebarGroup>
|
|
97
|
+
</SidebarContent>
|
|
98
|
+
<SidebarRail />
|
|
99
|
+
</Sidebar>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Fetching data in a component
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import * as React from 'react'
|
|
110
|
+
import { apiFetch } from '@core/lib/api'
|
|
111
|
+
import { LoadingSpinner } from '@core/components/ui/LoadingSpinner'
|
|
112
|
+
|
|
113
|
+
interface Item {
|
|
114
|
+
id: string
|
|
115
|
+
title: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ItemList() {
|
|
119
|
+
const [items, setItems] = React.useState<Item[]>([])
|
|
120
|
+
const [loading, setLoading] = React.useState(true)
|
|
121
|
+
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
apiFetch('/api/custom_my-feature?action=list')
|
|
124
|
+
.then(r => r.json())
|
|
125
|
+
.then(({ data }) => setItems(data || []))
|
|
126
|
+
.finally(() => setLoading(false))
|
|
127
|
+
}, [])
|
|
128
|
+
|
|
129
|
+
if (loading) return <LoadingSpinner />
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<ul className="space-y-2">
|
|
133
|
+
{items.map(item => (
|
|
134
|
+
<li key={item.id} className="p-3 border rounded">
|
|
135
|
+
{item.title}
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Available core UI components
|
|
146
|
+
|
|
147
|
+
All from `@core/components/ui/...` (shadcn/ui):
|
|
148
|
+
|
|
149
|
+
| Import | Component |
|
|
150
|
+
|--------|-----------|
|
|
151
|
+
| `@core/components/ui/button` | `Button` |
|
|
152
|
+
| `@core/components/ui/badge` | `Badge` |
|
|
153
|
+
| `@core/components/ui/card` | `Card`, `CardHeader`, `CardContent` |
|
|
154
|
+
| `@core/components/ui/dialog` | `Dialog`, `DialogContent`, `DialogHeader` |
|
|
155
|
+
| `@core/components/ui/input` | `Input` |
|
|
156
|
+
| `@core/components/ui/label` | `Label` |
|
|
157
|
+
| `@core/components/ui/select` | `Select`, `SelectContent`, `SelectItem` |
|
|
158
|
+
| `@core/components/ui/sidebar` | `Sidebar`, `SidebarMenu`, `SidebarMenuItem`, etc. |
|
|
159
|
+
| `@core/components/ui/tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` |
|
|
160
|
+
| `@core/components/ui/tooltip` | `Tooltip`, `TooltipProvider` |
|
|
161
|
+
| `@core/components/ui/LoadingSpinner` | `LoadingSpinner` |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Available core hooks
|
|
166
|
+
|
|
167
|
+
| Import | Hook | Purpose |
|
|
168
|
+
|--------|------|---------|
|
|
169
|
+
| `@core/contexts/AuthContext` | `useAuth()` | Current user, login/logout |
|
|
170
|
+
| `@core/contexts/AppContext` | `useCurrentApp()` | Current app record |
|
|
171
|
+
| `@core/lib/api` | `apiFetch()` | Auth-aware fetch wrapper |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Next steps
|
|
176
|
+
|
|
177
|
+
- [04-webhook-handlers.md](./04-webhook-handlers.md) — React to system events
|
|
178
|
+
- [05-testing.md](./05-testing.md) — Test your components and functions
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Guide 04: Webhook Handlers
|
|
2
|
+
|
|
3
|
+
## What this guide covers
|
|
4
|
+
|
|
5
|
+
How to register a custom webhook handler that reacts to integration events without modifying core.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Core's `integration-routes.ts` resolves webhook handlers dynamically from the `webhook_handlers` database table at runtime. Custom handlers self-register into that table — core never imports them directly.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Integration event arrives
|
|
15
|
+
→ core looks up handler name in webhook_handlers table
|
|
16
|
+
→ calls the registered Netlify function
|
|
17
|
+
→ your custom function runs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Step 1: Create the handler function
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// custom/functions/custom_my-handler.ts
|
|
26
|
+
import { createHandler, registerWebhookHandler } from '@core/_shared'
|
|
27
|
+
|
|
28
|
+
// Self-register on cold start
|
|
29
|
+
registerWebhookHandler({
|
|
30
|
+
name: 'my-handler',
|
|
31
|
+
functionName: 'custom_my-handler',
|
|
32
|
+
description: 'Handles item.created events from my integration',
|
|
33
|
+
events: ['item.created', 'item.updated'],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const handler = createHandler(async (event, ctx) => {
|
|
37
|
+
const payload = ctx.body
|
|
38
|
+
|
|
39
|
+
// payload.event — event name e.g. 'item.created'
|
|
40
|
+
// payload.data — the item/record that triggered the event
|
|
41
|
+
// ctx.principal — the system or user that triggered it
|
|
42
|
+
// ctx.db — RLS-scoped database client
|
|
43
|
+
|
|
44
|
+
console.log('Received event:', payload.event)
|
|
45
|
+
|
|
46
|
+
// Do your work
|
|
47
|
+
const { error } = await ctx.db
|
|
48
|
+
.from('items')
|
|
49
|
+
.insert({
|
|
50
|
+
type_slug: 'my_log',
|
|
51
|
+
data: { source_event: payload.event, source_id: payload.data?.id },
|
|
52
|
+
account_id: ctx.principal.account_id,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (error) throw error
|
|
56
|
+
|
|
57
|
+
return { received: true }
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## registerWebhookHandler options
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
registerWebhookHandler({
|
|
67
|
+
name: string // Unique handler name — used to look up this handler
|
|
68
|
+
functionName: string // Netlify function name (must match the file prefix)
|
|
69
|
+
description?: string // Human-readable description
|
|
70
|
+
events?: string[] // Event names this handler responds to (informational)
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Registration is idempotent — calling it multiple times (e.g. on every cold start) is safe. It upserts by `name`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Step 2: Register the handler in the database
|
|
79
|
+
|
|
80
|
+
The `registerWebhookHandler` call runs on cold start and writes to `webhook_handlers`. You can also seed it manually:
|
|
81
|
+
|
|
82
|
+
```sql
|
|
83
|
+
INSERT INTO webhook_handlers (name, function_name, description, events, is_active)
|
|
84
|
+
VALUES (
|
|
85
|
+
'my-handler',
|
|
86
|
+
'custom_my-handler',
|
|
87
|
+
'Handles item.created events',
|
|
88
|
+
'["item.created", "item.updated"]',
|
|
89
|
+
true
|
|
90
|
+
)
|
|
91
|
+
ON CONFLICT (name) DO UPDATE
|
|
92
|
+
SET function_name = EXCLUDED.function_name,
|
|
93
|
+
is_active = EXCLUDED.is_active;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Step 3: Route events to your handler
|
|
99
|
+
|
|
100
|
+
Integration routes look up the handler by name. To route an incoming webhook to your handler, the integration config should reference `"my-handler"` as the handler name. This is typically set in the `integrations` table or the integration's config payload.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Step 4: Assemble and test
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run assemble:functions
|
|
108
|
+
|
|
109
|
+
# Trigger manually (replace with your webhook payload)
|
|
110
|
+
curl -X POST http://localhost:8888/api/integration-routes \
|
|
111
|
+
-H "Content-Type: application/json" \
|
|
112
|
+
-d '{"handler": "my-handler", "event": "item.created", "data": {"id": "123"}}'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Verify registration
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
SELECT name, function_name, events, is_active
|
|
121
|
+
FROM webhook_handlers
|
|
122
|
+
WHERE name = 'my-handler';
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Next steps
|
|
128
|
+
|
|
129
|
+
- [05-testing.md](./05-testing.md) — Test your handler with the core test harness
|