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 +14 -3
- package/bin/create.js +30 -10
- package/package.json +3 -2
- package/scaffold.d.ts +7 -2
- package/scaffold.js +45 -9
- package/template/README.md +251 -0
- package/template/feltkit/app.ts +22 -9
- package/template/package.json +6 -1
- package/template-managed/.dockerignore +7 -0
- package/template-managed/Dockerfile +24 -0
- package/template-managed/README.md +222 -0
- package/template-managed/_gitignore +7 -0
- package/template-managed/docker-compose.yml +17 -0
- package/template-managed/feltdb.contract.json +1102 -0
- package/template-managed/feltkit/_generated/api.ts +9 -0
- package/template-managed/feltkit/app.ts +40 -0
- package/template-managed/feltkit/items.ts +55 -0
- package/template-managed/feltkit/system.ts +14 -0
- package/template-managed/fly.toml +28 -0
- package/template-managed/package.json +35 -0
- package/template-managed/scripts/deploy-fly.mjs +54 -0
- package/template-managed/scripts/managed-env.mjs +103 -0
- package/template-managed/tests/config.test.ts +91 -0
- package/template-managed/tests/inventory.test.ts +69 -0
- package/template-managed/tests/managed.live.test.ts +75 -0
- package/template-managed/tsconfig.json +15 -0
package/README.md
CHANGED
|
@@ -7,8 +7,19 @@ npm install
|
|
|
7
7
|
npm run dev
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Creates a FeltKit application
|
|
11
|
-
|
|
12
|
-
|
|
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>]
|
|
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
|
|
22
|
-
const
|
|
23
|
-
const
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
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
|
|
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
|
|
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",
|
|
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
|
-
|
|
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(
|
|
61
|
-
|
|
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>
|
package/template/feltkit/app.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
21
|
-
// (credential.*),
|
|
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
|
|
33
|
-
// model the agent decides with
|
|
34
|
-
services: {
|
|
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", {
|
package/template/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@feltdb/feltkit": "0.3.
|
|
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,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"]
|