create-feltkit 0.3.1 → 0.3.3

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 (40) hide show
  1. package/README.md +21 -3
  2. package/bin/create.js +30 -10
  3. package/package.json +3 -2
  4. package/scaffold.d.ts +7 -2
  5. package/scaffold.js +45 -9
  6. package/template/README.md +318 -0
  7. package/template/feltkit/_generated/api.ts +2 -1
  8. package/template/feltkit/app.ts +27 -10
  9. package/template/feltkit/inbox.ts +21 -0
  10. package/template/feltkit/projects.ts +19 -0
  11. package/template/feltkit/tasks.ts +11 -1
  12. package/template/index.html +4 -1
  13. package/template/package.json +6 -1
  14. package/template/public/apple-touch-icon.png +0 -0
  15. package/template/public/favicon.png +0 -0
  16. package/template/public/feltkit-mark.png +0 -0
  17. package/template/public/theme.js +7 -0
  18. package/template/src/App.tsx +31 -7
  19. package/template/src/styles.css +31 -31
  20. package/template/src/theme.tsx +58 -0
  21. package/template/tests/tasks.test.ts +42 -0
  22. package/template-managed/.dockerignore +7 -0
  23. package/template-managed/Dockerfile +24 -0
  24. package/template-managed/README.md +222 -0
  25. package/template-managed/_gitignore +7 -0
  26. package/template-managed/docker-compose.yml +17 -0
  27. package/template-managed/feltdb.contract.json +1102 -0
  28. package/template-managed/feltkit/_generated/api.ts +9 -0
  29. package/template-managed/feltkit/app.ts +40 -0
  30. package/template-managed/feltkit/items.ts +55 -0
  31. package/template-managed/feltkit/system.ts +14 -0
  32. package/template-managed/fly.toml +28 -0
  33. package/template-managed/package.json +35 -0
  34. package/template-managed/scripts/deploy-fly.mjs +54 -0
  35. package/template-managed/scripts/managed-env.mjs +103 -0
  36. package/template-managed/tests/config.test.ts +91 -0
  37. package/template-managed/tests/inventory.test.ts +69 -0
  38. package/template-managed/tests/managed.live.test.ts +75 -0
  39. package/template-managed/tsconfig.json +15 -0
  40. package/template/public/favicon.svg +0 -1
package/README.md CHANGED
@@ -1,3 +1,10 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rkendel1/feltkit/main/docs/assets/feltkit-logo-dark.png">
4
+ <img src="https://raw.githubusercontent.com/rkendel1/feltkit/main/docs/assets/feltkit-logo.png" alt="FeltKit" width="160">
5
+ </picture>
6
+ </p>
7
+
1
8
  # create-feltkit
2
9
 
3
10
  ```bash
@@ -7,8 +14,19 @@ npm install
7
14
  npm run dev
8
15
  ```
9
16
 
10
- Creates a FeltKit application: a small task manager with projects, tasks, notes
11
- (AppPort Services Files), three roles, live queries, AppPort capabilities, and tests.
12
- Refuses to write into a non-empty directory. Has no dependencies.
17
+ Creates a FeltKit application. Refuses to write into a non-empty directory. Has no dependencies.
18
+
19
+ | Starter | |
20
+ |---|---|
21
+ | `tasks` (default) | A small task manager with projects, tasks, notes (AppPort Services Files), three roles, live queries, AppPort capabilities, and tests. State in local FeltDB. |
22
+ | `managed` | An inventory API whose state lives in **Managed FeltDB**: the FeltDB contract checked in, keys and contract publication through FeltDB's CLI, a Dockerfile, compose file and Fly.io configuration, and tests. |
23
+
24
+ ```bash
25
+ npm create feltkit@latest inventory -- --template managed # or: -- --managed
26
+ cd inventory && npm install && npm test
27
+ npm run managed:setup
28
+ ```
29
+
30
+ The managed starter's README walks through setup, deployment and verification.
13
31
 
14
32
  Documentation: https://github.com/rkendel1/concave#readme
package/bin/create.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // npm create feltkit@latest <dir>
2
+ // npm create feltkit@latest <dir> [-- --template managed]
3
3
  import { resolve } from "node:path";
4
- import { scaffold, VERSION } from "../scaffold.js";
4
+ import { scaffold, TEMPLATES, VERSION } from "../scaffold.js";
5
5
 
6
6
  const args = process.argv.slice(2);
7
7
  const color = process.stdout.isTTY && !process.env.NO_COLOR;
@@ -11,24 +11,44 @@ const green = (s) => (color ? `\u001b[32m${s}\u001b[39m` : s);
11
11
  const red = (s) => (color ? `\u001b[31m${s}\u001b[39m` : s);
12
12
 
13
13
  if (args.includes("--help") || args.includes("-h")) {
14
- console.log(`npm create feltkit@latest <directory> [-- --name <app-name>]\n\nCreates a FeltKit application (a small task manager you can run immediately).`);
14
+ console.log(`npm create feltkit@latest <directory> [-- --name <app-name>] [-- --template <starter>]
15
+
16
+ Creates a FeltKit application you can run immediately.
17
+
18
+ Starters (--template):
19
+ ${Object.entries(TEMPLATES).map(([name, { description }]) => ` ${name.padEnd(9)}${description}`).join("\n")}
20
+
21
+ --managed is short for --template managed.`);
15
22
  process.exit(0);
16
23
  }
17
24
  if (args.includes("--version")) {
18
25
  console.log(VERSION);
19
26
  process.exit(0);
20
27
  }
21
- const nameIndex = args.indexOf("--name");
22
- const name = nameIndex >= 0 ? args[nameIndex + 1] : undefined;
23
- const dir = args.find((a, i) => !a.startsWith("-") && (nameIndex < 0 || i !== nameIndex + 1)) ?? "my-feltkit-app";
28
+ const VALUE_FLAGS = ["--name", "--template"];
29
+ const valueOf = (flag) => {
30
+ const index = args.indexOf(flag);
31
+ return index >= 0 ? args[index + 1] : undefined;
32
+ };
33
+ const name = valueOf("--name");
34
+ const template = args.includes("--managed") ? "managed" : (valueOf("--template") ?? "tasks");
35
+ const dir = args.find((a, i) => !a.startsWith("-") && !VALUE_FLAGS.includes(args[i - 1])) ?? "my-feltkit-app";
24
36
 
25
37
  try {
26
- const result = scaffold({ target: resolve(process.cwd(), dir), ...(name ? { name } : {}) });
27
- console.log(`${green("✓")} Created ${bold(result.name)} in ${result.dir}\n`);
38
+ const result = scaffold({ target: resolve(process.cwd(), dir), template, ...(name ? { name } : {}) });
39
+ console.log(`${green("✓")} Created ${bold(result.name)} in ${result.dir}${template === "tasks" ? "" : ` (${template} starter)`}\n`);
28
40
  console.log(` cd ${dir}`);
29
41
  console.log(" npm install");
30
- console.log(" npm run dev\n");
31
- console.log(dim("The dev server prints your app URL and a dashboard link. State lives in .feltkit/development/state."));
42
+ if (template === "managed") {
43
+ console.log(" npm test # local FeltDB, no account needed");
44
+ console.log(" npm run managed:setup # Managed FeltDB: your account, a least-privilege key, the contract\n");
45
+ console.log(dim("State lives in Managed FeltDB once set up (README.md walks through setup, deploy and verification)."));
46
+ console.log(dim("Keys are written to .env.local and never committed; deploy with Docker or `npm run deploy:fly`."));
47
+ } else {
48
+ console.log(" npm run dev\n");
49
+ console.log(dim("The dev server prints your app URL and a dashboard link. State lives in .feltkit/development/state."));
50
+ console.log(dim("README.md covers running, testing, deploying, and every feltkit command."));
51
+ }
32
52
  console.log(dim("Using a coding agent? `npx feltkit skills install` (after npm install) adds the FeltKit skills for Claude Code."));
33
53
  } catch (error) {
34
54
  console.error(`${red("✗")} ${error.message}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-feltkit",
3
- "version": "0.3.1",
4
- "description": "Create a FeltKit application: npm create feltkit@latest my-app",
3
+ "version": "0.3.3",
4
+ "description": "Create a FeltKit application: npm create feltkit@latest my-app (add -- --template managed for Managed FeltDB)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "scaffold.js",
27
27
  "scaffold.d.ts",
28
28
  "template",
29
+ "template-managed",
29
30
  "README.md"
30
31
  ],
31
32
  "scripts": {
package/scaffold.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type TemplateName = "tasks" | "managed";
1
2
  export interface ScaffoldOptions {
2
3
  /** Directory to create. Must not exist or be empty. */
3
4
  target: string;
@@ -5,17 +6,21 @@ export interface ScaffoldOptions {
5
6
  name?: string;
6
7
  /** Version spec for feltkit; defaults to this package's version (released in lockstep). */
7
8
  sdkSpec?: string;
9
+ /** Which starter: `tasks` (default, local FeltDB) or `managed` (Managed FeltDB). */
10
+ template?: TemplateName;
8
11
  }
9
12
  export interface ScaffoldResult {
10
13
  dir: string;
11
14
  name: string;
12
15
  sdkSpec: string;
16
+ template: TemplateName;
13
17
  }
14
18
  export class ScaffoldError extends Error {
15
- readonly code: "TARGET_NOT_EMPTY" | "TEMPLATE_MISSING" | "TEMPLATE_OUT_OF_SYNC";
19
+ readonly code: "TARGET_NOT_EMPTY" | "TEMPLATE_MISSING" | "TEMPLATE_OUT_OF_SYNC" | "TEMPLATE_UNKNOWN";
16
20
  readonly hint?: string;
17
21
  }
18
22
  export declare const VERSION: string;
23
+ export declare const TEMPLATES: Readonly<Record<TemplateName, { dir: string; description: string }>>;
19
24
  export declare function toAppName(input: string): string;
20
- export declare function templateDir(): string;
25
+ export declare function templateDir(template?: TemplateName): string;
21
26
  export declare function scaffold(options: ScaffoldOptions): ScaffoldResult;
package/scaffold.js CHANGED
@@ -1,10 +1,23 @@
1
- // Scaffold a FeltKit application from the starter template. No dependencies, so `npm create` is fast.
1
+ // Scaffold a FeltKit application from a starter template. No dependencies, so `npm create` is fast.
2
2
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { basename, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  export const VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
7
7
 
8
+ /**
9
+ * The starters. Both are the same FeltKit; they differ in where state lives and how it is deployed.
10
+ * tasks a task manager with a React frontend, AppPort Services (files, intelligence) and an agent;
11
+ * state in local FeltDB (`.feltkit/<env>/state`), or a FeltDB server you run.
12
+ * managed an inventory API whose state lives in Managed FeltDB: no AppPort Services (Managed FeltDB
13
+ * stores only what the application's contract declares), the contract checked in, keys from
14
+ * the FeltDB CLI, and a Dockerfile, compose file and Fly.io configuration.
15
+ */
16
+ export const TEMPLATES = Object.freeze({
17
+ tasks: { dir: "template", description: "Task manager with a frontend; local FeltDB (default)" },
18
+ managed: { dir: "template-managed", description: "Inventory API on Managed FeltDB; contract, keys and deployment as code" },
19
+ });
20
+
8
21
  export class ScaffoldError extends Error {
9
22
  constructor(code, message, hint) {
10
23
  super(message);
@@ -14,10 +27,14 @@ export class ScaffoldError extends Error {
14
27
  }
15
28
  }
16
29
 
17
- export function templateDir() {
18
- const dir = fileURLToPath(new URL("./template/", import.meta.url));
30
+ export function templateDir(template = "tasks") {
31
+ const entry = TEMPLATES[template];
32
+ if (!entry) {
33
+ throw new ScaffoldError("TEMPLATE_UNKNOWN", `There is no "${template}" starter.`, `Choose one of: ${Object.keys(TEMPLATES).join(", ")}.`);
34
+ }
35
+ const dir = fileURLToPath(new URL(`./${entry.dir}/`, import.meta.url));
19
36
  if (!existsSync(join(dir, "package.json"))) {
20
- throw new ScaffoldError("TEMPLATE_MISSING", "The starter template is missing from this installation of create-feltkit.", "Reinstall it: npm create feltkit@latest");
37
+ throw new ScaffoldError("TEMPLATE_MISSING", `The ${template} starter is missing from this installation of create-feltkit.`, "Reinstall it: npm create feltkit@latest");
21
38
  }
22
39
  return dir;
23
40
  }
@@ -33,13 +50,13 @@ export function toAppName(input) {
33
50
  return name || "my-app";
34
51
  }
35
52
 
36
- export function scaffold({ target, name, sdkSpec }) {
53
+ export function scaffold({ target, name, sdkSpec, template = "tasks" }) {
54
+ const source = templateDir(template);
37
55
  if (existsSync(target) && readdirSync(target).filter((f) => f !== ".git" && f !== ".DS_Store").length > 0) {
38
56
  throw new ScaffoldError("TARGET_NOT_EMPTY", `${target} already exists and is not empty. FeltKit will not write over existing files.`, "Choose a new directory name.");
39
57
  }
40
58
  const appName = toAppName(name ?? basename(target));
41
59
  const spec = sdkSpec ?? process.env.FELTKIT_SDK_SPEC ?? `^${VERSION}`;
42
- const source = templateDir();
43
60
  mkdirSync(target, { recursive: true });
44
61
  cpSync(source, target, { recursive: true });
45
62
  if (existsSync(join(target, "_gitignore"))) renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
@@ -52,11 +69,30 @@ export function scaffold({ target, name, sdkSpec }) {
52
69
  pkg.dependencies["@feltdb/feltkit"] = spec;
53
70
  writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
54
71
 
72
+ // The starter's own name, wherever it names the application.
55
73
  const appPath = join(target, "feltkit", "app.ts");
56
74
  const app = readFileSync(appPath, "utf8");
57
- if (!app.includes('name: "tasks"')) {
75
+ const starterName = /feltkit\(\{\s*name: "([a-z][a-z0-9-]*)"/.exec(app)?.[1];
76
+ if (!starterName) {
58
77
  throw new ScaffoldError("TEMPLATE_OUT_OF_SYNC", "The starter's feltkit/app.ts no longer declares its name; the template is out of sync.");
59
78
  }
60
- writeFileSync(appPath, app.replace('name: "tasks"', `name: ${JSON.stringify(appName)}`));
61
- return { dir: target, name: appName, sdkSpec: spec };
79
+ writeFileSync(appPath, app.replace(`name: "${starterName}"`, `name: ${JSON.stringify(appName)}`));
80
+ // The default starter's README is titled with the starter's name and names its Docker image after it.
81
+ const readmePath = join(target, "README.md");
82
+ if (existsSync(readmePath)) {
83
+ const readme = readFileSync(readmePath, "utf8");
84
+ if (readme.startsWith(`# ${starterName}\n`)) {
85
+ writeFileSync(
86
+ readmePath,
87
+ readme
88
+ .replace(`# ${starterName}\n`, `# ${appName}\n`)
89
+ .replaceAll(`docker build -t ${starterName} `, `docker build -t ${appName} `)
90
+ .replaceAll(`-v ${starterName}-data:/data ${starterName}`, `-v ${appName}-data:/data ${appName}`)
91
+ .replaceAll(`--name ${starterName} --namespace ${starterName} `, `--name ${appName} --namespace ${appName} `),
92
+ );
93
+ }
94
+ }
95
+ const flyPath = join(target, "fly.toml");
96
+ if (existsSync(flyPath)) writeFileSync(flyPath, readFileSync(flyPath, "utf8").replace(/^app = ".*"$/m, `app = ${JSON.stringify(appName)}`));
97
+ return { dir: target, name: appName, sdkSpec: spec, template };
62
98
  }
@@ -0,0 +1,318 @@
1
+ # tasks
2
+
3
+ A FeltKit application: a small task manager with projects, tasks and notes, roles, live
4
+ queries, AppPort Services, an agent, and a development dashboard (Studio). Each
5
+ environment keeps all of its state in one FeltDB instance.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ `npm run dev` prints:
15
+
16
+ ```
17
+ App http://localhost:3000
18
+ Dashboard http://localhost:3000/_feltkit/#token=…
19
+ AppPort http://localhost:3000/appport
20
+ MCP http://localhost:3000/mcp
21
+ State …/.feltkit/development/state
22
+ ```
23
+
24
+ - **App**: you are signed in as a development user. **Ada** (`member`, `admin`) can do
25
+ everything; **Grace** (`viewer`) can only read. Switch between them from the app's header.
26
+ - **Dashboard** (Studio): your data, functions, capabilities, services, agent, models and
27
+ every operation that ran. Keep the link private: the token in it is this environment's
28
+ admin token.
29
+ - Edit anything in `feltkit/`. The backend reloads in place and keeps its state, sessions and
30
+ history. The frontend in `src/` hot-reloads.
31
+
32
+ Development state lives in `.feltkit/development/` (ignored by git). Delete that folder to
33
+ start over.
34
+
35
+ ## What's in the project
36
+
37
+ | Path | What it is |
38
+ |---|---|
39
+ | `feltkit/app.ts` | The application: its name, roles and permissions, dev users, enabled services, collections |
40
+ | `feltkit/projects.ts`, `feltkit/tasks.ts` | Functions: queries (read) and mutations (write). A function is `<module>:<export>`, e.g. `tasks:create` |
41
+ | `feltkit/_generated/api.ts` | Typed references to every function. Regenerated by `feltkit dev` and `feltkit codegen`; don't edit it |
42
+ | `src/` | The React frontend. Colors are tokens in `src/styles.css`, each `light-dark(light, dark)`; the top bar's switch (`src/theme.tsx`) picks light, dark, or the system setting, remembered per browser and applied before first paint by `public/theme.js` |
43
+ | `tests/` | Tests, each against a fresh FeltDB instance |
44
+ | `.feltkit/` | Local state, keys and builds, per environment. Never commit it |
45
+
46
+ ## How it works
47
+
48
+ **Data.** Collections are declared in `feltkit/app.ts` with a schema; every write is
49
+ validated against it.
50
+
51
+ ```ts
52
+ export const tasks = app.collection("tasks", {
53
+ projectId: s.string(),
54
+ title: s.string({ minLength: 1, maxLength: 200 }),
55
+ done: s.boolean(),
56
+ });
57
+ ```
58
+
59
+ **Functions.** A query reads, a mutation writes. Writes in one mutation commit together, or
60
+ not at all. `access` is the permission the caller needs; `capability` publishes the
61
+ function through AppPort, so agents can use it too.
62
+
63
+ ```ts
64
+ export const create = app.mutation({
65
+ access: "tasks.write",
66
+ args: { projectId: s.string(), title: s.string({ minLength: 1 }) },
67
+ capability: { name: "tasks.create", description: "Add a task to a project." },
68
+ handler: async (ctx, { projectId, title }) =>
69
+ ctx.db.insert(tasks, { projectId, title, done: false, createdAt: Date.now(), createdBy: ctx.auth.requirePrincipal().id }),
70
+ });
71
+ ```
72
+
73
+ **Roles.** `roles` in `feltkit/app.ts` grant permissions; `"tasks.*"` grants every
74
+ `tasks.<x>`. The server checks them on every call, whatever the frontend shows.
75
+
76
+ **Frontend.** Queries are live: they update when the data changes.
77
+
78
+ ```tsx
79
+ import { useMutation, useQueryState } from "@feltdb/feltkit/react";
80
+ import { api } from "../feltkit/_generated/api";
81
+
82
+ const tasks = useQueryState(api.tasks.list, { projectId });
83
+ const create = useMutation(api.tasks.create);
84
+ ```
85
+
86
+ ## Services
87
+
88
+ Every AppPort Service is enabled in `feltkit/app.ts` and stored in the same FeltDB instance:
89
+ Files, Notifications, Configuration, API keys, Webhooks, Jobs, Schedules and Intelligence.
90
+ Configure each one in **Studio → Services**: every service has its own screen, and each
91
+ operation there is authorized as the dev user you choose with **Act as**. From a function:
92
+
93
+ ```ts
94
+ await ctx.services.files.upload({ name: "notes.txt", content: text });
95
+ await ctx.services.notifications.send({ recipient: "grace", type: "task.completed", title: "Done" });
96
+ await ctx.services.webhooks?.emit("task.created", { id }); // mutations only
97
+ await ctx.services.jobs?.enqueue("projects.clear-completed", {}); // mutations only
98
+ const greeting = await ctx.services.configuration.get("GREETING");
99
+ ```
100
+
101
+ The starter already uses them: creating and completing a task emits `task.created` and
102
+ `task.completed` to any registered webhook endpoint; completing someone else's task notifies
103
+ its creator; and the job `projects.clear-completed` runs `projects:clearCompleted`.
104
+
105
+ Declare what your app sends in `feltkit/app.ts`, so Studio's forms offer it in dropdowns:
106
+
107
+ ```ts
108
+ services: {
109
+ notifications: { types: ["task.completed"] },
110
+ webhooks: { events: ["task.created", "task.completed"] },
111
+ jobs: { handlers: { "projects.clear-completed": "projects:clearCompleted" } },
112
+ // …
113
+ }
114
+ ```
115
+
116
+ A job type runs the mutation you name, as the user who queued it, with retries. To turn a
117
+ service off, remove it from `services`.
118
+
119
+ ## Connecting an API
120
+
121
+ **Studio → Integrations → Connect an API** reads an OpenAPI document (JSON) or lets you
122
+ describe the operations yourself, then writes `feltkit/<name>.ts` and grants `<name>.*` to the
123
+ roles you choose. Each operation becomes a capability your functions, the agent and other apps
124
+ can call (`<name>.<operation>`), with its permission, and every call recorded. The API key is
125
+ stored in FeltKit's custody; in production set `<NAME>_URL` and `FELTKIT_CREDENTIAL_<NAME>`.
126
+ Then narrow each operation's `output` schema in that file to what the API returns.
127
+
128
+ ## Choosing a model for the agent
129
+
130
+ Open **Studio → Models**, pick a provider and model, type its API key, then **Save and use**
131
+ and **Test**. **Save for later** keeps more models (other providers too) under **Saved
132
+ models**, each with its own key; **Use** switches the agent to one. Or from a terminal:
133
+
134
+ ```bash
135
+ npx feltkit model set --provider anthropic --model <model> --api-key-stdin
136
+ ```
137
+
138
+ The key is stored encrypted outside FeltDB (`.feltkit/development/credentials.vault`) and is
139
+ never shown again. Never put a key in `feltkit/`, `src/`, tests or a committed file.
140
+
141
+ ## Testing
142
+
143
+ ```bash
144
+ npm test
145
+ ```
146
+
147
+ runs `tests/**/*.test.ts`. Each `createTestApp()` gets its own temporary FeltDB instance,
148
+ and `t.as("ada")` calls functions as a dev user:
149
+
150
+ ```ts
151
+ const t = await createTestApp();
152
+ const projectId = await t.as("ada").mutation(api.projects.create, { name: "Launch" });
153
+ await assert.rejects(t.as("grace").mutation(api.tasks.create, { projectId, title: "x" }), { code: "FORBIDDEN" });
154
+ await t.close();
155
+ ```
156
+
157
+ ## Deploying
158
+
159
+ FeltKit doesn't host your app; it builds what you run, for the way you want to run it:
160
+
161
+ | Target | What runs | Data |
162
+ |---|---|---|
163
+ | `container` (default) | Everything in one container | FeltDB state on a volume |
164
+ | `hosted` | A separate app container | Managed FeltDB, hosted for you, from the artifact's contract (it declares AppPort Services' collections too) |
165
+ | `self-hosted` | A separate app container | A FeltDB server you operate (`feltdb-server`), in this app's namespace |
166
+
167
+ Each target's `DEPLOY.md` spells out its contract: what to set, where state lives and how to
168
+ recover it, what stops startup, and how to upgrade and roll back. A target never changes where
169
+ state is stored: the container target refuses `FELTDB_URL`, and the others refuse to start
170
+ without their FeltDB settings.
171
+
172
+ 1. **Check what production needs.**
173
+
174
+ ```bash
175
+ npx feltkit readiness --env production
176
+ ```
177
+
178
+ 2. **Build and verify.** It asks which target (or pass `--target container`, `hosted` or `self-hosted`):
179
+
180
+ ```bash
181
+ npm run deploy
182
+ ```
183
+
184
+ This builds the backend and frontend, boots the production build against throwaway state
185
+ to prove it serves, and writes `.feltkit/deploy/` with a `Dockerfile`, `docker-compose.yml`,
186
+ `fly.toml`, `.env.example` (exactly the variables it needs) and `DEPLOY.md`. Each deploy
187
+ replaces it, keeping production state a previous run kept inside it. It prints the commands
188
+ to run next.
189
+
190
+ 3. **Run it.** Everything in one container:
191
+
192
+ ```bash
193
+ docker compose -f .feltkit/deploy/docker-compose.yml up --build
194
+ ```
195
+
196
+ Or with Node, from the artifact (data in its `.feltkit/production`):
197
+
198
+ ```bash
199
+ cd .feltkit/deploy
200
+ ```
201
+
202
+ ```bash
203
+ npm ci --omit=dev
204
+ ```
205
+
206
+ ```bash
207
+ npm start
208
+ ```
209
+
210
+ With Managed FeltDB (`--target hosted`), create the app and its key from the artifact's
211
+ contract (`npx feltdb init-managed --contract .feltkit/deploy/feltdb.contract.json
212
+ --runtime-scopes minimal`), put `FELTDB_URL`, `FELTDB_APPLICATION_ID` and `FELTDB_TOKEN` in
213
+ `.feltkit/deploy/.env`, then run the same `docker compose` command. With your own FeltDB
214
+ server (`--target self-hosted`), create a key for the app's namespace (`feltdb-server keys
215
+ create --keys <keys.json> --name tasks --namespace tasks --scope state:read,state:write`) and
216
+ put `FELTDB_URL` and `FELTDB_TOKEN` in `.env`. Either container refuses to start without its
217
+ settings, and checks the database answers, so it never keeps data on its own disk. For
218
+ Fly.io, plain Docker, or other hosts, follow `.feltkit/deploy/DEPLOY.md`.
219
+
220
+ Open <http://localhost:3000>.
221
+
222
+ 4. **Sign in.** Production has no development sign-in (Ada and Grace are for development).
223
+ Until you connect your own login, create a session token, *with the app stopped* (only one
224
+ process may open the state), and paste it into the app's sign-in screen:
225
+
226
+ ```bash
227
+ npx feltkit session create --env production --type user --id you --role member
228
+ ```
229
+
230
+ Run it in `.feltkit/deploy` for the Node run (or anywhere with the target's `FELTDB_*`
231
+ variables set: then the app may keep running, since its sessions are in FeltDB). For the one-container Docker run, use a one-off container on the same
232
+ volume while the app's container is stopped:
233
+
234
+ ```bash
235
+ docker run --rm -v tasks-data:/data tasks node node_modules/@feltdb/feltkit/bin/feltkit.js session create --env production --type user --id you --role member
236
+ ```
237
+
238
+ For real users, add `auth.authenticate` to `feltkit({ ... })` to verify requests with your
239
+ identity provider.
240
+
241
+ **Production settings** (from your secret manager, never in the repository). With one
242
+ container, none is required:
243
+
244
+ | Variable | What it's for |
245
+ |---|---|
246
+ | `FELTDB_URL`, `FELTDB_APPLICATION_ID`, `FELTDB_TOKEN` | Managed FeltDB (`--target hosted`) |
247
+ | `FELTDB_URL`, `FELTDB_TOKEN` | Your FeltDB server (`--target self-hosted`) |
248
+ | `FELTKIT_WEBHOOK_PRIVATE_NETWORKS` | `1` lets webhooks reach receivers on private networks (refused by default outside development) |
249
+ | `FELTKIT_API_KEY_VERIFIER_KEY` | Hosted and self-hosted: 32+ random bytes (`openssl rand -base64 32`). In one container it is generated once and kept beside the state on the volume |
250
+ | `FELTKIT_CREDENTIAL_<ID>` | Credential values, e.g. `FELTKIT_CREDENTIAL_ANTHROPIC` for `credential-ref:anthropic`. Keys you entered in Studio are development-only and are not deployed |
251
+ | `FELTKIT_STATE_DIR` | Where local state lives (the container image uses `/data/state`, on the volume) |
252
+ | `FELTKIT_OPERATOR_KEY` | Optional: signs operator sign-ins for production Studio |
253
+ | `PORT` | The port to listen on (default 3000) |
254
+
255
+ **Production Studio.** Studio isn't served in production unless `FELTKIT_DASHBOARD=1`. Then
256
+ operators sign in with a one-time grant you create on the host
257
+ (`npx feltkit operator grant --id <id> --role <role> --env production`). Production Studio
258
+ is for operating the app; data edits and service configuration stay development tools.
259
+ To move non-secret configuration between environments, use
260
+ `npx feltkit config export` and `npx feltkit config import <file> --env production --dry-run`.
261
+
262
+ To deploy on Managed FeltDB, where no host keeps state, start from
263
+ `npm create feltkit@latest <dir> -- --template managed`; its README walks through setup.
264
+
265
+ ## Commands
266
+
267
+ npm scripts:
268
+
269
+ | Script | Runs |
270
+ |---|---|
271
+ | `npm run dev` | `feltkit dev`: backend, frontend, Studio and hot reload |
272
+ | `npm test` | `feltkit test`: the tests, against isolated test environments |
273
+ | `npm run build` | `feltkit build`: bundle the backend, build the frontend, verify the bundle |
274
+ | `npm start` | `feltkit start`: run the production build |
275
+ | `npm run deploy` | `feltkit deploy`: build a verified deployment artifact |
276
+ | `npm run inspect` | `feltkit inspect`: status, state location, functions, services, recent activity |
277
+ | `npm run typecheck` | `tsc --noEmit` |
278
+
279
+ Every `feltkit` command (run as `npx feltkit <command>`; `--help` on any of them for its options):
280
+
281
+ | Command | What it does |
282
+ |---|---|
283
+ | `dev [--port 3000]` | Development environment: backend, frontend, Studio, hot reload |
284
+ | `build` | Bundle the backend, build the frontend, load the bundle to verify it |
285
+ | `start [--port $PORT]` | Run the production build (the `production` environment) |
286
+ | `test [files…]` | Run `tests/**/*.test.ts` against isolated test environments |
287
+ | `deploy [--out <dir>]` | Build, verify the production build boots, write a Node/Docker artifact |
288
+ | `inspect [--json]` | What the app is, where its state lives, and what it's doing |
289
+ | `readiness [--env production]` | What an environment has and lacks before production, with fixes |
290
+ | `model show` / `model set` | Show or choose the model that makes agent decisions |
291
+ | `credential set` / `status` / `remove` | Store an API key (from a hidden prompt or stdin, never an argument), see where each credential comes from, or erase one |
292
+ | `config export` / `import` | Move non-secret configuration (variables, webhook endpoints, …) between environments |
293
+ | `session create` / `list` / `revoke` | Sessions for agents and service accounts |
294
+ | `operator grant` | A one-time production Studio sign-in for an operator |
295
+ | `codegen` | Regenerate `feltkit/_generated/api.ts` |
296
+ | `contract` | Write the FeltDB application contract (Managed FeltDB) |
297
+ | `doctor` | Check Node, dependencies and running servers for problems |
298
+ | `skills install` / `check` | Add or validate the FeltKit skills for coding agents (Claude Code) |
299
+
300
+ ## Using a coding agent
301
+
302
+ ```bash
303
+ npx feltkit skills install
304
+ ```
305
+
306
+ adds the FeltKit skills to `.claude/skills/`, so Claude Code knows how to build, test and
307
+ operate this app.
308
+
309
+ ## When something's wrong
310
+
311
+ - `npx feltkit doctor` checks Node, dependencies and running servers.
312
+ - `npx feltkit inspect` shows the environment's state, functions and recent errors.
313
+ - Studio → **Activity** shows every operation: who ran it, what was decided, and what FeltDB
314
+ committed.
315
+ - Errors carry a code (`FORBIDDEN`, `INVALID_ARGUMENT`, `NOT_FOUND`, …) and usually a hint with
316
+ the fix.
317
+
318
+ FeltKit documentation: <https://github.com/rkendel1/feltkit#readme>
@@ -2,8 +2,9 @@
2
2
  /* eslint-disable */
3
3
  import { createApi } from "@feltdb/feltkit/client";
4
4
  import type * as app_module from "../app.js";
5
+ import type * as inbox_module from "../inbox.js";
5
6
  import type * as projects_module from "../projects.js";
6
7
  import type * as tasks_module from "../tasks.js";
7
8
 
8
9
  /** Typed references to every query and mutation. Type-only imports: no backend code reaches the browser. */
9
- export const api = createApi<{ "app": typeof app_module; "projects": typeof projects_module; "tasks": typeof tasks_module }>();
10
+ export const api = createApi<{ "app": typeof app_module; "inbox": typeof inbox_module; "projects": typeof projects_module; "tasks": typeof tasks_module }>();
@@ -12,15 +12,19 @@ export const app = feltkit({
12
12
  // Roles grant AppPort permissions. "tasks.*" grants every tasks.<x> permission.
13
13
  roles: {
14
14
  // Members use the app and Studio's agent (feltkit.agent.*: create, approve, and follow agent tasks).
15
- member: ["projects.*", "tasks.*", "files.*", "feltkit.agent.*"],
15
+ // notifications.*: a principal reads, marks, and dismisses only its own notifications.
16
+ // webhooks.emit + webhooks.deliver: their changes notify registered endpoints (task.created,
17
+ // task.completed); each delivery runs as the person whose change emitted it.
18
+ member: ["projects.*", "tasks.*", "files.*", "notifications.*", "webhooks.emit", "webhooks.deliver", "feltkit.agent.*"],
16
19
  // Viewers read. They can see agent tasks but not start or approve them.
17
- viewer: ["projects.read", "tasks.read", "files.read", "feltkit.agent.read"],
20
+ viewer: ["projects.read", "tasks.read", "files.read", "notifications.read", "feltkit.agent.read"],
18
21
  // Agents can read, complete tasks, and leave notes. They cannot create or delete.
19
- agent: ["projects.read", "tasks.read", "tasks.complete", "tasks.note", "files.read", "files.write"],
20
- // Admins configure the app: choose the model (intelligence.*), store API keys
21
- // (credential.*), and change configuration. Keys are stored by reference; the value
22
- // never enters FeltDB.
23
- admin: ["intelligence.*", "credential.*", "configuration.*"],
22
+ agent: ["projects.read", "tasks.read", "tasks.complete", "tasks.note", "files.read", "files.write", "notifications.send", "webhooks.emit", "webhooks.deliver"],
23
+ // Admins configure the app in Studio → Services: the model (intelligence.*), stored
24
+ // credentials (credential.*), configuration, API keys, webhooks, jobs, and schedules.
25
+ // Credentials are stored by reference; the value never enters FeltDB.
26
+ // feltkit.operations.*: in production, admins are Studio's operators (npx feltkit operator grant).
27
+ admin: ["intelligence.*", "credential.*", "configuration.*", "apikeys.*", "webhooks.*", "jobs.*", "schedules.*", "feltkit.operations.*"],
24
28
  },
25
29
  // Development-only identities. Sign in as them from the app or the dashboard.
26
30
  dev: {
@@ -29,9 +33,22 @@ export const app = feltkit({
29
33
  { id: "grace", name: "Grace Hopper", roles: ["viewer"] },
30
34
  ],
31
35
  },
32
- // AppPort Services, stored in this app's FeltDB instance: Files, and Intelligence (which
33
- // model the agent decides with; choose one and enter its API key in Studio → Models).
34
- services: { files: true, intelligence: true },
36
+ // AppPort Services, stored in this app's FeltDB instance, each configured in Studio →
37
+ // Services. Intelligence is the model the agent decides with (Studio → Models).
38
+ services: {
39
+ files: true,
40
+ // The types this app sends; Studio offers them when you send one by hand.
41
+ notifications: { types: ["task.completed"] },
42
+ configuration: true,
43
+ // A request authenticated by an API key holds these roles.
44
+ apiKeys: { roles: ["member"] },
45
+ // The events this app emits (feltkit/tasks.ts); Studio offers them when you register an endpoint.
46
+ webhooks: { events: ["task.created", "task.completed"] },
47
+ // Job type → the mutation that runs it, as the principal that queued it, with retries.
48
+ jobs: { handlers: { "projects.clear-completed": "projects:clearCompleted" } },
49
+ schedules: true,
50
+ intelligence: true,
51
+ },
35
52
  });
36
53
 
37
54
  export const projects = app.collection("projects", {
@@ -0,0 +1,21 @@
1
+ import { s } from "@feltdb/feltkit";
2
+ import { app } from "./app.js";
3
+
4
+ /**
5
+ * The signed-in person's notifications (AppPort Services Notifications, e.g. "task.completed"
6
+ * from tasks:setDone). Each principal reads and marks only its own; the service enforces it.
7
+ */
8
+ export const list = app.query({
9
+ access: "notifications.read",
10
+ args: { unread: s.optional(s.boolean()) },
11
+ handler: async (ctx, { unread }) =>
12
+ (await ctx.services.notifications.list(unread ? { unread: true } : {})).map((n) => ({ id: n.id, type: n.type, title: n.title, data: n.data ?? null, createdAt: n.createdAt, read: Boolean(n.readAt) })),
13
+ });
14
+
15
+ export const markRead = app.mutation({
16
+ access: "notifications.update",
17
+ args: { id: s.string() },
18
+ handler: async (ctx, { id }) => {
19
+ await ctx.services.notifications.markRead(id);
20
+ },
21
+ });