create-feltkit 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -7,8 +7,19 @@ npm install
7
7
  npm run dev
8
8
  ```
9
9
 
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.
10
+ Creates a FeltKit application. Refuses to write into a non-empty directory. Has no dependencies.
11
+
12
+ | Starter | |
13
+ |---|---|
14
+ | `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. |
15
+ | `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. |
16
+
17
+ ```bash
18
+ npm create feltkit@latest inventory -- --template managed # or: -- --managed
19
+ cd inventory && npm install && npm test
20
+ npm run managed:setup
21
+ ```
22
+
23
+ The managed starter's README walks through setup, deployment and verification.
13
24
 
14
25
  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.2",
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`, `-v ${appName}-data:/data`)
91
+ .replaceAll(`--env-file prod.env ${starterName}`, `--env-file prod.env ${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,251 @@
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 |
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.assigned", title: "Review the plan" });
96
+ const greeting = await ctx.services.configuration.get("GREETING");
97
+ await ctx.services.jobs?.enqueue("report.build", { week }); // mutations only
98
+ ```
99
+
100
+ A job type runs the function you name in `services.jobs.handlers`
101
+ (`jobs: { handlers: { "report.build": "reports:build" } }`), as the user who queued it.
102
+ To turn a service off, remove it from `services`.
103
+
104
+ ## Choosing a model for the agent
105
+
106
+ Open **Studio → Models**, pick a provider and model, type its API key, **Save**, then
107
+ **Test**. Or from a terminal:
108
+
109
+ ```bash
110
+ npx feltkit model set --provider anthropic --model <model> --api-key-stdin
111
+ ```
112
+
113
+ The key is stored encrypted outside FeltDB (`.feltkit/development/credentials.vault`) and is
114
+ never shown again. Never put a key in `feltkit/`, `src/`, tests or a committed file.
115
+
116
+ ## Testing
117
+
118
+ ```bash
119
+ npm test
120
+ ```
121
+
122
+ runs `tests/**/*.test.ts`. Each `createTestApp()` gets its own temporary FeltDB instance,
123
+ and `t.as("ada")` calls functions as a dev user:
124
+
125
+ ```ts
126
+ const t = await createTestApp();
127
+ const projectId = await t.as("ada").mutation(api.projects.create, { name: "Launch" });
128
+ await assert.rejects(t.as("grace").mutation(api.tasks.create, { projectId, title: "x" }), { code: "FORBIDDEN" });
129
+ await t.close();
130
+ ```
131
+
132
+ ## Deploying
133
+
134
+ FeltKit doesn't host your app. It builds an artifact you run on any Node 20+ host or
135
+ container.
136
+
137
+ 1. **Check what production needs.**
138
+
139
+ ```bash
140
+ npx feltkit readiness --env production
141
+ ```
142
+
143
+ This lists what the environment has and lacks (state, secrets, identity, model,
144
+ integrations), with the fix for each. It exits with 1 while anything fails.
145
+
146
+ 2. **Build and verify the artifact.**
147
+
148
+ ```bash
149
+ npm run deploy
150
+ ```
151
+
152
+ This builds the backend and the frontend, boots the production build against throwaway
153
+ state to prove it serves, and writes `.feltkit/deploy/<app>-<time>/` (or `--out <dir>`)
154
+ with `package.json`, a `Dockerfile` and `DEPLOY.md`.
155
+
156
+ 3. **Run it.** From the artifact directory:
157
+
158
+ ```bash
159
+ npm install --omit=dev
160
+ PORT=3000 npm start
161
+ ```
162
+
163
+ Or with Docker:
164
+
165
+ ```bash
166
+ docker build -t tasks . && docker run -p 3000:3000 -v tasks-data:/data --env-file prod.env tasks
167
+ ```
168
+
169
+ **Production settings** (set them from your secret manager, never in the repository):
170
+
171
+ | Variable | What it's for |
172
+ |---|---|
173
+ | `FELTKIT_STATE_DIR` | Where production state lives on a durable volume (the Docker image uses `/data/state`) |
174
+ | `FELTDB_URL`, `FELTDB_TOKEN` | Use a FeltDB server instead of local state |
175
+ | `FELTKIT_API_KEY_VERIFIER_KEY` | **Required**, because API keys are enabled: 32+ random bytes. Changing it invalidates every issued API key |
176
+ | `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 |
177
+ | `FELTKIT_OPERATOR_KEY` | Optional: signs operator sign-ins for production Studio |
178
+ | `PORT` | The port to listen on |
179
+
180
+ **Users in production.** Development sign-in (Ada, Grace) is off in production. Verify
181
+ requests with your identity provider by adding `auth.authenticate` to `feltkit({ ... })`,
182
+ or issue sessions to known principals:
183
+
184
+ ```bash
185
+ npx feltkit session create --env production --id ops-bot --type agent --role agent
186
+ ```
187
+
188
+ **Production Studio.** Studio isn't served in production unless `FELTKIT_DASHBOARD=1`. Then
189
+ operators sign in with a one-time grant you create on the host
190
+ (`npx feltkit operator grant --id <id> --role <role> --env production`). Production Studio
191
+ is for operating the app; data edits and service configuration stay development tools.
192
+ To move non-secret configuration between environments, use
193
+ `npx feltkit config export` and `npx feltkit config import <file> --env production --dry-run`.
194
+
195
+ To deploy on Managed FeltDB, where no host keeps state, start from
196
+ `npm create feltkit@latest <dir> -- --template managed`; its README walks through setup.
197
+
198
+ ## Commands
199
+
200
+ npm scripts:
201
+
202
+ | Script | Runs |
203
+ |---|---|
204
+ | `npm run dev` | `feltkit dev`: backend, frontend, Studio and hot reload |
205
+ | `npm test` | `feltkit test`: the tests, against isolated test environments |
206
+ | `npm run build` | `feltkit build`: bundle the backend, build the frontend, verify the bundle |
207
+ | `npm start` | `feltkit start`: run the production build |
208
+ | `npm run deploy` | `feltkit deploy`: build a verified deployment artifact |
209
+ | `npm run inspect` | `feltkit inspect`: status, state location, functions, services, recent activity |
210
+ | `npm run typecheck` | `tsc --noEmit` |
211
+
212
+ Every `feltkit` command (run as `npx feltkit <command>`; `--help` on any of them for its options):
213
+
214
+ | Command | What it does |
215
+ |---|---|
216
+ | `dev [--port 3000]` | Development environment: backend, frontend, Studio, hot reload |
217
+ | `build` | Bundle the backend, build the frontend, load the bundle to verify it |
218
+ | `start [--port $PORT]` | Run the production build (the `production` environment) |
219
+ | `test [files…]` | Run `tests/**/*.test.ts` against isolated test environments |
220
+ | `deploy [--out <dir>]` | Build, verify the production build boots, write a Node/Docker artifact |
221
+ | `inspect [--json]` | What the app is, where its state lives, and what it's doing |
222
+ | `readiness [--env production]` | What an environment has and lacks before production, with fixes |
223
+ | `model show` / `model set` | Show or choose the model that makes agent decisions |
224
+ | `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 |
225
+ | `config export` / `import` | Move non-secret configuration (variables, webhook endpoints, …) between environments |
226
+ | `session create` / `list` / `revoke` | Sessions for agents and service accounts |
227
+ | `operator grant` | A one-time production Studio sign-in for an operator |
228
+ | `codegen` | Regenerate `feltkit/_generated/api.ts` |
229
+ | `contract` | Write the FeltDB application contract (Managed FeltDB) |
230
+ | `doctor` | Check Node, dependencies and running servers for problems |
231
+ | `skills install` / `check` | Add or validate the FeltKit skills for coding agents (Claude Code) |
232
+
233
+ ## Using a coding agent
234
+
235
+ ```bash
236
+ npx feltkit skills install
237
+ ```
238
+
239
+ adds the FeltKit skills to `.claude/skills/`, so Claude Code knows how to build, test and
240
+ operate this app.
241
+
242
+ ## When something's wrong
243
+
244
+ - `npx feltkit doctor` checks Node, dependencies and running servers.
245
+ - `npx feltkit inspect` shows the environment's state, functions and recent errors.
246
+ - Studio → **Activity** shows every operation: who ran it, what was decided, and what FeltDB
247
+ committed.
248
+ - Errors carry a code (`FORBIDDEN`, `INVALID_ARGUMENT`, `NOT_FOUND`, …) and usually a hint with
249
+ the fix.
250
+
251
+ FeltKit documentation: <https://github.com/rkendel1/feltkit#readme>
@@ -12,15 +12,16 @@ 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
+ member: ["projects.*", "tasks.*", "files.*", "notifications.*", "feltkit.agent.*"],
16
17
  // Viewers read. They can see agent tasks but not start or approve them.
17
- viewer: ["projects.read", "tasks.read", "files.read", "feltkit.agent.read"],
18
+ viewer: ["projects.read", "tasks.read", "files.read", "notifications.read", "feltkit.agent.read"],
18
19
  // Agents can read, complete tasks, and leave notes. They cannot create or delete.
19
20
  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.*"],
21
+ // Admins configure the app in Studio → Services: the model (intelligence.*), stored
22
+ // credentials (credential.*), configuration, API keys, webhooks, jobs, and schedules.
23
+ // Credentials are stored by reference; the value never enters FeltDB.
24
+ admin: ["intelligence.*", "credential.*", "configuration.*", "apikeys.*", "webhooks.*", "jobs.*", "schedules.*"],
24
25
  },
25
26
  // Development-only identities. Sign in as them from the app or the dashboard.
26
27
  dev: {
@@ -29,9 +30,21 @@ export const app = feltkit({
29
30
  { id: "grace", name: "Grace Hopper", roles: ["viewer"] },
30
31
  ],
31
32
  },
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 },
33
+ // AppPort Services, stored in this app's FeltDB instance, each configured in Studio →
34
+ // Services. Intelligence is the model the agent decides with (Studio → Models).
35
+ services: {
36
+ files: true,
37
+ notifications: true,
38
+ configuration: true,
39
+ // A request authenticated by an API key holds these roles.
40
+ apiKeys: { roles: ["member"] },
41
+ webhooks: true,
42
+ // Job type → the function that runs it, as the principal that queued it:
43
+ // jobs: { handlers: { "report.build": "reports:build" } }
44
+ jobs: true,
45
+ schedules: true,
46
+ intelligence: true,
47
+ },
35
48
  });
36
49
 
37
50
  export const projects = app.collection("projects", {
@@ -13,7 +13,7 @@
13
13
  "typecheck": "tsc --noEmit"
14
14
  },
15
15
  "dependencies": {
16
- "@feltdb/feltkit": "0.3.1",
16
+ "@feltdb/feltkit": "0.3.2",
17
17
  "react": "19.2.0",
18
18
  "react-dom": "19.2.0"
19
19
  },
@@ -24,5 +24,10 @@
24
24
  "@vitejs/plugin-react": "5.1.0",
25
25
  "typescript": "5.9.3",
26
26
  "vite": "7.3.7"
27
+ },
28
+ "overrides": {
29
+ "@appport/services": {
30
+ "@feltdb/core": "0.16.0"
31
+ }
27
32
  }
28
33
  }
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .feltkit
3
+ dist
4
+ tests
5
+ .env
6
+ .env.*
7
+ .feltdb
@@ -0,0 +1,24 @@
1
+ # The inventory service on Managed FeltDB. The image holds no state and no credentials:
2
+ # FELTDB_URL, FELTDB_APPLICATION_ID and FELTDB_ENVIRONMENT come from the platform's configuration
3
+ # and FELTDB_TOKEN from its secret store, at run time (README.md, "Deploy").
4
+ FROM node:22-slim AS build
5
+ WORKDIR /app
6
+ COPY package.json package-lock.json* ./
7
+ RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
8
+ COPY . .
9
+ # The contract checked in must be the one this source declares: the managed application
10
+ # only stores what it declares.
11
+ RUN npx feltkit contract --check && npx feltkit build && npm prune --omit=dev --no-audit --no-fund
12
+
13
+ FROM node:22-slim
14
+ ENV NODE_ENV=production PORT=8080
15
+ WORKDIR /app
16
+ COPY --from=build --chown=node:node /app ./
17
+ USER node
18
+ EXPOSE 8080
19
+ # Liveness: the runtime is ready only after it reached FeltDB with its key at startup.
20
+ HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
21
+ CMD ["node", "-e", "fetch('http://127.0.0.1:'+process.env.PORT+'/_feltkit/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
22
+ # The launcher validates the configuration (refusing to start without it) and forwards
23
+ # SIGTERM so FeltKit shuts down cleanly.
24
+ CMD ["node", "scripts/managed-env.mjs", "start", "--host", "0.0.0.0"]