@promptowl/contextnest-community 1.16.1 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +415 -415
- package/LICENSE.md +142 -142
- package/README.md +164 -169
- package/dist/{chunk-QJVRZT6M.js → chunk-4YGJSUJC.js} +35 -5
- package/dist/{chunk-OTGZQZKK.js → chunk-HWDPCPYT.js} +10 -9
- package/dist/{chunk-L7UCMGWZ.js → chunk-SNFGU5Q7.js} +166 -81
- package/dist/{chunk-D6HICFVW.js → chunk-UXJSO54R.js} +1 -1
- package/dist/{chunk-CLKZZZS2.js → chunk-YF4OFT2V.js} +1 -1
- package/dist/{grants-service-I535QVTW.js → grants-service-4NXPBYWD.js} +3 -3
- package/dist/index.js +213 -65
- package/dist/{review-service-V2GR6VM3.js → review-service-3CCKDO5N.js} +9 -7
- package/dist/{stewardship-service-LTREGVSJ.js → stewardship-service-OHTTZFND.js} +2 -2
- package/dist/{version-service-V6K5SZ3P.js → version-service-EFZGGL3Y.js} +2 -2
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-D1299AIf.css +1 -0
- package/dist/web3/assets/index-W6tEA3RK.js +1046 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -165
- package/dist/web3/assets/index-DnQARUL3.css +0 -1
- package/dist/web3/assets/index-_m7WyozM.js +0 -1051
package/CONFIGURATION.md
CHANGED
|
@@ -1,415 +1,415 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
All server configuration is driven by environment variables. Set them in your shell, in a `.env` file, in a Docker image, or via your process manager — however you deploy the rest of your stack.
|
|
4
|
-
|
|
5
|
-
## Auth modes
|
|
6
|
-
|
|
7
|
-
The server supports two authentication modes. You pick one per instance with `AUTH_MODE`.
|
|
8
|
-
|
|
9
|
-
### `AUTH_MODE=key` (default)
|
|
10
|
-
|
|
11
|
-
Every request requires an `Authorization: Bearer cnst_...` header. Use this for any multi-user or internet-facing deployment.
|
|
12
|
-
|
|
13
|
-
- Users register via `POST /auth/register` (email + password) or log in with PromptOwl via `POST /auth/device` → `POST /auth/promptowl`.
|
|
14
|
-
- API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use.
|
|
15
|
-
- Rate-limited login / register / device-auth endpoints (sliding window, per IP + per email).
|
|
16
|
-
- First PromptOwl-authenticated user becomes the server admin (atomic claim). Admin can invite teammates at `POST /auth/invite`.
|
|
17
|
-
|
|
18
|
-
Clients include the token on every request:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
curl -H 'Authorization: Bearer cnst_<your-token>' http://your-server/nests
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### `AUTH_MODE=open`
|
|
25
|
-
|
|
26
|
-
No authentication required. Every request is attributed to the anonymous admin user (`00000000-...`). Everyone effectively owns every anon-created nest.
|
|
27
|
-
|
|
28
|
-
**Use this only when:**
|
|
29
|
-
- You're running locally for yourself (`bind 127.0.0.1`), or
|
|
30
|
-
- You're on a trusted LAN and don't care about access controls, or
|
|
31
|
-
- You're behind an upstream reverse proxy that already authenticates.
|
|
32
|
-
|
|
33
|
-
**Don't use this when:**
|
|
34
|
-
- The port is reachable from the public internet
|
|
35
|
-
- Multiple people share the deployment and need isolated data
|
|
36
|
-
- You need audit trails (every write shows up under "anonymous admin")
|
|
37
|
-
|
|
38
|
-
The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Full env var reference
|
|
43
|
-
|
|
44
|
-
| Var | Default | Purpose |
|
|
45
|
-
|---|---|---|
|
|
46
|
-
| `PORT` | `3838` | HTTP port the server listens on. |
|
|
47
|
-
| `DATA_ROOT` | `./data` (relative to cwd) | Root directory for SQLite DB + nest filesystem vaults. Set to an absolute path in production so it's independent of where you `cd`'d from. |
|
|
48
|
-
| `DATABASE_PATH` | `$DATA_ROOT/community.db` | Override the SQLite file location explicitly. (SQLite backend only.) |
|
|
49
|
-
| `NEST_STORAGE_ROOT` | `$DATA_ROOT/nests` | Root directory for nest vault files (markdown + version history). Override to store nests separately from the DB — e.g. point it at a Cloud Storage (GCS) volume mount on Cloud Run (`/mnt/nests`) while the SQLite DB stays on `DATA_ROOT` off the mount. **Moving existing nests:** the app does not relocate files — copy `$DATA_ROOT/nests/*` to the new root (e.g. `gsutil rsync`) before switching, or existing nests will appear empty (DB rows present, files missing). On a GCS mount, also set `DRIFT_SCAN_INTERVAL_MS=0` to avoid constant FUSE scans, and run a single instance (GCS FUSE has no file locking — concurrent writers lose data). |
|
|
50
|
-
| `DB_DRIVER` | `sqlite` | Database backend: `sqlite` (default) or `postgres`. When unset, the server infers `postgres` if `DATABASE_URL` or `CLOUD_SQL_CONNECTION_NAME` is set, else `sqlite`. Existing deployments need no change — they stay on SQLite. See [Database backends](#database-backends). |
|
|
51
|
-
| `DATABASE_URL` | `""` | PostgreSQL connection string for a TCP connection, e.g. `postgres://user:pass@host:5432/dbname`. Setting it selects the Postgres backend (unless `DB_DRIVER=sqlite`). |
|
|
52
|
-
| `CLOUD_SQL_CONNECTION_NAME` | `""` | Cloud SQL instance connection name (`project:region:instance`). When set, the server connects over the Cloud SQL Auth Proxy unix socket at `/cloudsql/<name>` — the recommended Cloud Run setup. Combine with `DB_USER`, `DB_PASSWORD`, `DB_NAME`. |
|
|
53
|
-
| `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` | `localhost` / `5432` / `""` / `""` / `""` | Discrete Postgres connection params, used when neither `CLOUD_SQL_CONNECTION_NAME` nor `DATABASE_URL` is set. |
|
|
54
|
-
| `DB_POOL_MAX` | `10` | Max Postgres pool connections. Keep modest — Cloud SQL tiers cap total connections. |
|
|
55
|
-
| `DB_SSL` | `false` | Set `true` to require TLS on a Postgres TCP connection (not needed over the Cloud SQL unix socket, which is already secure). |
|
|
56
|
-
| `DB_SSL_CA` | `""` | Path to a CA certificate (PEM) for verify-ca/verify-full TLS when `DB_SSL=true`. |
|
|
57
|
-
| `AUTH_MODE` | `key` | `key` or `open`. See above. |
|
|
58
|
-
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
59
|
-
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
60
|
-
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
61
|
-
| `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
|
|
62
|
-
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Official deployment only — leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
|
|
63
|
-
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) |
|
|
64
|
-
| `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
|
|
65
|
-
| `OIDC_ISSUER` | `""` | OIDC issuer URL, e.g. `https://login.microsoftonline.com/<tenant>/v2.0` (Microsoft Entra ID) or `https://accounts.google.com` (Google). **`https://` only** — a non-https value is rejected with a warning and SSO stays off. Must serve `<issuer>/.well-known/openid-configuration`. |
|
|
66
|
-
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
|
67
|
-
| `OIDC_CLIENT_SECRET` | `""` | Client secret from your IdP app registration. Write-only on the Settings API — `GET /admin/settings` reports only `oidc_client_secret_set: true/false`, never the value. |
|
|
68
|
-
| `OIDC_ALLOWED_DOMAINS` | `""` (any) | Comma-separated email-domain allowlist, e.g. `acme.com, contractors.acme.com`. When set, only accounts whose asserted email is on a listed domain may sign in (others bounce with `domain_not_allowed`). Empty allows any domain the IdP asserts. |
|
|
69
|
-
| `OIDC_AUTO_PROVISION` | `true` | Create a user automatically on first successful OIDC sign-in (display name from the `name` claim). Set `false` to allow only pre-existing (invited/registered) users — unknown emails bounce with `not_invited`. |
|
|
70
|
-
| `OIDC_DEPARTMENT_TAGGING` | `false` | Auto-tag newly **created** documents with the creator's directory department: `dept:<slugified-department>` (lowercase, spaces → dashes, e.g. `dept:customer-success`) is appended to the document's tags, deduped against user-supplied tags. Applies on create only — never on update, never retroactively — and a user without a stored department is a silent no-op. The department is captured from the OIDC `department` ID-token claim on every SSO login (a login without the claim clears it, so directory moves propagate), so this is only meaningful when your IdP emits that claim — see [Department auto-tagging](#department-auto-tagging). Also editable from Settings → Single sign-on. |
|
|
71
|
-
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
|
|
72
|
-
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
73
|
-
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
74
|
-
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
75
|
-
| `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
|
|
76
|
-
| `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
|
|
77
|
-
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
|
|
78
|
-
| `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
|
|
79
|
-
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
|
|
80
|
-
| `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
|
|
81
|
-
| `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
|
|
82
|
-
| `TYPE_TABLE_ENABLED` | `true` | Same as above for **table** nodes. |
|
|
83
|
-
| `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
|
|
84
|
-
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from Settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
|
|
85
|
-
| `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
|
|
86
|
-
| `SUBAGENT_MAX_CHILDREN` | `16` | Max direct children a single run may spawn (clamped 1..128) — bounds the call tree's WIDTH. Together with `SUBAGENT_MAX_DEPTH` this caps total tree size so a runner can't fork-bomb the DB. |
|
|
87
|
-
| `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
|
|
88
|
-
| `RUN_MAX_CONCURRENT_ROOTS` | `50` | Max concurrently-running root (depth-0) runs per nest (clamped 1..1000). The subagent caps bound a single tree; this bounds how many trees can run at once, so triggers can't flood a nest. |
|
|
89
|
-
| `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
|
|
90
|
-
| `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
|
|
91
|
-
| `MSTEAMS_WEBHOOK_URL` | _(unset — connector off)_ | Microsoft Teams incoming-webhook URL for the same governance events, posted as Adaptive Cards. `https://` only — the URL embeds a secret. Also editable from Settings in the UI. See [Microsoft Teams notifications](#microsoft-teams-notifications). |
|
|
92
|
-
| `SMTP_URL` | _(unset — connector off)_ | SMTP connection URL for email notifications (`smtp://` or `smtps://`, credentials inline). Requires `NOTIFY_EMAIL_FROM` and `NOTIFY_EMAIL_TO`. Also editable from Settings. |
|
|
93
|
-
| `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
|
|
94
|
-
| `NOTIFY_EMAIL_TO` | _(unset)_ | Comma-separated recipients for notification emails. |
|
|
95
|
-
| `NOTIFY_DEBOUNCE_MS` | `15000` | Per-nest buffer window before notifications flush; bursts collapse into one digest message. |
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Microsoft Teams notifications
|
|
100
|
-
|
|
101
|
-
Set `MSTEAMS_WEBHOOK_URL` (or paste the URL into **Settings → Notifications →
|
|
102
|
-
Microsoft Teams notifications**) and the server posts the same governance
|
|
103
|
-
events the Slack connector covers — review requested / approved / rejected,
|
|
104
|
-
collaborator added, plus per-nest burst digests — to one Teams channel as
|
|
105
|
-
[Adaptive Cards](https://adaptivecards.io/). Leave it empty to turn the
|
|
106
|
-
connector off. Delivery is fire-and-forget with a short timeout: a Teams
|
|
107
|
-
outage or a bad URL never slows down or fails the action that triggered the
|
|
108
|
-
message.
|
|
109
|
-
|
|
110
|
-
> Naming note: the setting is `msteams_webhook_url` / `MSTEAMS_WEBHOOK_URL`
|
|
111
|
-
> because "teams" in this product means user groups (the `/teams` API), not
|
|
112
|
-
> Microsoft Teams.
|
|
113
|
-
|
|
114
|
-
### Create the incoming webhook in a Teams channel
|
|
115
|
-
|
|
116
|
-
Microsoft retired the classic Office 365 "Incoming Webhook" connectors;
|
|
117
|
-
webhooks are now created with the **Workflows** (Power Automate) app:
|
|
118
|
-
|
|
119
|
-
1. In Microsoft Teams, open the channel that should receive notifications,
|
|
120
|
-
click **⋯ (More options) → Workflows**.
|
|
121
|
-
2. Pick the template **"Post to a channel when a webhook request is
|
|
122
|
-
received"** (searching for "webhook" finds it), sign in if prompted, and
|
|
123
|
-
confirm the team + channel.
|
|
124
|
-
3. Create the flow and **copy the HTTP POST URL** it shows (a
|
|
125
|
-
`https://….logic.azure.com/…` or `https://….powerplatform.com/…` address).
|
|
126
|
-
4. Paste that URL into **Settings → Notifications → Microsoft Teams
|
|
127
|
-
notifications** (or set `MSTEAMS_WEBHOOK_URL`).
|
|
128
|
-
5. Save, then click **Send test** on the card to post a test message and
|
|
129
|
-
confirm the channel receives it. (The button tests the *saved* URL — save
|
|
130
|
-
first, then test. The Slack card has the same button.)
|
|
131
|
-
|
|
132
|
-
If your tenant still has a legacy Office 365 connector webhook, it is expected
|
|
133
|
-
to accept the same payload envelope (legacy connectors documented support for
|
|
134
|
-
Adaptive Card attachments, though we have not verified one end-to-end — the
|
|
135
|
-
card's plain-text `fallbackText` is the safety net) — but create new hooks via
|
|
136
|
-
Workflows.
|
|
137
|
-
|
|
138
|
-
The URL embeds a secret — anyone holding it can post to the channel, so treat
|
|
139
|
-
it like a password (it is why the setting requires `https://`). The server
|
|
140
|
-
sends the standard envelope Workflows expects:
|
|
141
|
-
`{"type": "message", "attachments": [{"contentType":
|
|
142
|
-
"application/vnd.microsoft.card.adaptive", "content": {…card…}}]}` with a
|
|
143
|
-
plain-text `fallbackText` for clients that cannot render cards.
|
|
144
|
-
|
|
145
|
-
Per-nest connector rows post the same Adaptive Card format to a nest-specific
|
|
146
|
-
webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
|
|
147
|
-
body like `{"channel": "teams", "url": "https://…", "events":
|
|
148
|
-
["review_requested", "review_rejected"]}` (`events` may also be `["*"]`; the
|
|
149
|
-
`url` may be an `env:KEY` reference into the nest's env store). Manage rows
|
|
150
|
-
with `GET`/`PATCH`/`DELETE` on the same path.
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Single sign-on (OIDC)
|
|
155
|
-
|
|
156
|
-
Generic OpenID Connect sign-in against any spec-compliant identity provider —
|
|
157
|
-
Microsoft Entra ID and Google are the first-class presets in **Settings →
|
|
158
|
-
Single sign-on**. The server runs a standard authorization-code flow with PKCE:
|
|
159
|
-
`GET /auth/oidc/login` redirects to your IdP, `GET /auth/oidc/callback` verifies
|
|
160
|
-
the returned ID token (issuer, audience, nonce, signature against the issuer's
|
|
161
|
-
JWKS) and starts a normal browser session. Users are looked up by email;
|
|
162
|
-
unknown emails are created automatically when `OIDC_AUTO_PROVISION` is on.
|
|
163
|
-
|
|
164
|
-
All seven `OIDC_*` values are also editable at runtime from **Settings → Single
|
|
165
|
-
sign-on** (superadmin only) — no restart needed. The Settings page shows the
|
|
166
|
-
exact **redirect URI** to register with your IdP:
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
<PUBLIC_BASE_URL or server origin>/auth/oidc/callback
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Set `PUBLIC_BASE_URL` when the server sits behind a reverse proxy so the
|
|
173
|
-
redirect URI is derived from the canonical address rather than the incoming
|
|
174
|
-
Host header.
|
|
175
|
-
|
|
176
|
-
### Connect Microsoft Entra ID
|
|
177
|
-
|
|
178
|
-
1. **Register an app.** Entra admin center → *Identity → Applications → App
|
|
179
|
-
registrations → New registration*. Name it (e.g. "ContextNest"), leave the
|
|
180
|
-
account type at *Accounts in this organizational directory only*.
|
|
181
|
-
2. **Add the redirect URI.** In the registration: *Authentication → Add a
|
|
182
|
-
platform → Web*, and paste the redirect URI shown on the ContextNest
|
|
183
|
-
Settings page (`https://<your-server>/auth/oidc/callback`).
|
|
184
|
-
3. **Create a client secret.** *Certificates & secrets → New client secret*.
|
|
185
|
-
Copy the secret **value** (not the ID) immediately — it's shown once.
|
|
186
|
-
4. **Collect the IDs.** On the app's *Overview* page copy the **Application
|
|
187
|
-
(client) ID** and the **Directory (tenant) ID**.
|
|
188
|
-
5. **Fill in ContextNest.** Settings → Single sign-on: click **Microsoft Entra
|
|
189
|
-
ID**, paste the tenant ID when prompted (issuer becomes
|
|
190
|
-
`https://login.microsoftonline.com/<tenant>/v2.0`), then paste the client
|
|
191
|
-
ID and the secret value. Optionally restrict **Allowed email domains** to
|
|
192
|
-
your org's domain. Save, then flip the toggle **On** and save again (or in
|
|
193
|
-
the same save).
|
|
194
|
-
6. **Test.** Open the login page in a private window — a **Sign in with
|
|
195
|
-
Microsoft** button appears and round-trips through Entra. Entra accounts
|
|
196
|
-
without an `email` claim fall back to `preferred_username` (the UPN).
|
|
197
|
-
|
|
198
|
-
For **Google**: create an OAuth client ID (type *Web application*) in the
|
|
199
|
-
Google Cloud console, add the same redirect URI under *Authorized redirect
|
|
200
|
-
URIs*, and use the **Google** preset (issuer `https://accounts.google.com`)
|
|
201
|
-
with that client ID/secret.
|
|
202
|
-
|
|
203
|
-
### Department auto-tagging
|
|
204
|
-
|
|
205
|
-
With `OIDC_DEPARTMENT_TAGGING` on (Settings → Single sign-on → **Department
|
|
206
|
-
auto-tagging**), every document a user **creates** is tagged
|
|
207
|
-
`dept:<slugified-department>` from their directory department — e.g. a user in
|
|
208
|
-
*Customer Success* creates docs tagged `dept:customer-success`. Create-only:
|
|
209
|
-
edits never add or change the tag, and existing documents are never
|
|
210
|
-
retro-tagged. Users without a department (password accounts, or an IdP that
|
|
211
|
-
doesn't emit the claim) create untagged documents — never an error.
|
|
212
|
-
|
|
213
|
-
The department is read from the **`department` claim** in the OIDC ID token
|
|
214
|
-
and stored on the user at every SSO login: a new value updates it, an absent
|
|
215
|
-
claim clears it, so directory moves propagate on the user's next sign-in.
|
|
216
|
-
|
|
217
|
-
Microsoft Entra ID does **not** emit the claim by default — add it to the app
|
|
218
|
-
registration: *Token configuration → Add optional claim → Token type: **ID**
|
|
219
|
-
→ select **department** → Add* (grant the suggested Microsoft Graph
|
|
220
|
-
permission if prompted), and make sure the users' *Department* field is
|
|
221
|
-
populated in Entra. Other IdPs work too as long as they emit a string
|
|
222
|
-
`department` claim in the ID token (e.g. a Keycloak user-attribute mapper).
|
|
223
|
-
|
|
224
|
-
> **Privacy note:** the `dept:<slug>` tag becomes part of the document's
|
|
225
|
-
> visible metadata — anyone who can read the document (collaborators, shared
|
|
226
|
-
> nests, public nests) can see the creator's directory department. That's
|
|
227
|
-
> PII-adjacent organizational data; consider this before enabling on servers
|
|
228
|
-
> where documents are shared beyond the creator's own team or made public.
|
|
229
|
-
|
|
230
|
-
### Revoking access (offboarding)
|
|
231
|
-
|
|
232
|
-
Disabling a user **at the IdP** only blocks *new* sign-ins — an existing
|
|
233
|
-
ContextNest session stays valid until it expires (30 days). To cut access
|
|
234
|
-
immediately, **remove the user in ContextNest** (Settings → users): that wipes
|
|
235
|
-
all of their active sessions at once. Real offboarding is therefore two steps —
|
|
236
|
-
disable at the IdP *and* remove in ContextNest — with the ContextNest step being
|
|
237
|
-
the one that ends live sessions.
|
|
238
|
-
|
|
239
|
-
When a user signs themselves out, ContextNest performs an **RP-initiated
|
|
240
|
-
logout**: if the IdP advertises an `end_session_endpoint` (Entra, Okta,
|
|
241
|
-
Keycloak do; Google does not), the browser is bounced through it so the IdP
|
|
242
|
-
session ends too and the next "Sign in with SSO" click doesn't silently
|
|
243
|
-
re-authenticate. Where the IdP has no logout endpoint, the local session is
|
|
244
|
-
cleared and the user lands back on the login page (the IdP session persists —
|
|
245
|
-
that's the IdP's own timeout to manage).
|
|
246
|
-
|
|
247
|
-
### Email-verification trust assumption
|
|
248
|
-
|
|
249
|
-
An ID token whose `email_verified` claim is **explicitly `false`** is refused
|
|
250
|
-
(`?sso_error=email_not_verified`). When the claim is **absent**, the server
|
|
251
|
-
trusts the email claim — Microsoft Entra ID v2 tokens frequently omit it, and
|
|
252
|
-
both Entra and Google guarantee address ownership, so refusing on absence
|
|
253
|
-
would break the primary providers. This is a deliberate trust assumption:
|
|
254
|
-
**when your IdP allows unverified self-registered emails (e.g. an open
|
|
255
|
-
Keycloak realm), configure `oidc_allowed_domains` and disable self-registration
|
|
256
|
-
at the IdP** — otherwise anyone able to assert an arbitrary email at your IdP
|
|
257
|
-
could sign in as the matching local account.
|
|
258
|
-
|
|
259
|
-
Sign-in failures bounce back to the app as `/?sso_error=<code>` and surface as
|
|
260
|
-
a toast; codes: `disabled`, `not_configured`, `discovery_failed`,
|
|
261
|
-
`provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`,
|
|
262
|
-
`email_not_verified`, `domain_not_allowed`, `not_invited`, `rate_limited`,
|
|
263
|
-
`service_error`.
|
|
264
|
-
|
|
265
|
-
---
|
|
266
|
-
|
|
267
|
-
## Database backends
|
|
268
|
-
|
|
269
|
-
The governance/auth metadata (users, sessions, nests registry, stewards, reviews,
|
|
270
|
-
versions index, comments, telemetry) is stored in a relational database. Two
|
|
271
|
-
backends are supported:
|
|
272
|
-
|
|
273
|
-
- **SQLite (default).** A single file at `DATABASE_PATH`. Zero-config, ideal for
|
|
274
|
-
single-box and local deployments. Nothing changes for existing installs.
|
|
275
|
-
- **PostgreSQL / Cloud SQL.** For deployments where local disk isn't durable
|
|
276
|
-
(e.g. Google Cloud Run, where the container filesystem is ephemeral). Selected
|
|
277
|
-
by setting `DB_DRIVER=postgres`, `DATABASE_URL`, or `CLOUD_SQL_CONNECTION_NAME`.
|
|
278
|
-
|
|
279
|
-
On first connect the server creates its schema automatically (idempotent) — no
|
|
280
|
-
manual migration step. There is no built-in SQLite→Postgres data migration; a new
|
|
281
|
-
Postgres backend starts empty.
|
|
282
|
-
|
|
283
|
-
### First-boot upgrade passes
|
|
284
|
-
|
|
285
|
-
Some upgrades carry a one-shot data pass that runs at startup, before the server
|
|
286
|
-
accepts requests, and records itself in `schema_migrations` so it never repeats.
|
|
287
|
-
Two exist today: the `node_versions` backfill from `history.yaml`, and the
|
|
288
|
-
version-chain repair added in 1.13 (re-anchors documents whose version chain was
|
|
289
|
-
grafted by an older import, and moves inline patches into `v{N}.diff` files).
|
|
290
|
-
|
|
291
|
-
Both walk every document in every nest, so on a large vault the **first** boot
|
|
292
|
-
after upgrading can take noticeably longer than usual — reads of the on-disk
|
|
293
|
-
vault, not database work, so an install on network/FUSE storage feels it most.
|
|
294
|
-
Every subsequent boot skips the pass entirely. Progress and a per-pass summary
|
|
295
|
-
are printed to the server log.
|
|
296
|
-
|
|
297
|
-
> ⚠️ **Cloud Run durability — read this.** Cloud SQL persists only the *metadata
|
|
298
|
-
> database*. The actual **document content and version history** live on the
|
|
299
|
-
> filesystem under `$DATA_ROOT/nests/`, which on Cloud Run is **ephemeral** and is
|
|
300
|
-
> lost when an instance is recycled. Using Cloud SQL alone does **not** make a
|
|
301
|
-
> Cloud Run deployment fully durable. You must also put `$DATA_ROOT` on persistent,
|
|
302
|
-
> shared storage — e.g. mount a GCS bucket via Cloud Storage FUSE, or use a
|
|
303
|
-
> persistent volume — otherwise documents will disappear on redeploy/scale-in.
|
|
304
|
-
|
|
305
|
-
### Runtime settings persistence
|
|
306
|
-
|
|
307
|
-
Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
|
|
308
|
-
sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/Teams/SMTP connectors)
|
|
309
|
-
plus the **installed license key** — are stored in the database (`server_settings`
|
|
310
|
-
table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
|
|
311
|
-
filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
|
|
312
|
-
horizontally-scaled instance had its own filesystem, so a change on one never
|
|
313
|
-
reached the others. The database (Cloud SQL in production) is durable **and** shared,
|
|
314
|
-
so a UI change persists across rebuilds and is seen by every instance. Rows are
|
|
315
|
-
loaded into the process environment at boot.
|
|
316
|
-
|
|
317
|
-
**Precedence between the deploy environment and a UI change.** Each stored row
|
|
318
|
-
remembers the deploy-env value in effect when it was written. At boot:
|
|
319
|
-
|
|
320
|
-
- If the deploy env value for that key is **unchanged**, the stored UI change wins
|
|
321
|
-
(your Settings-page edit survives an identical rebuild).
|
|
322
|
-
- If the deploy env value **changed** (you redeployed with a new value for that
|
|
323
|
-
variable), the deploy env wins and the stored row is re-synced to it.
|
|
324
|
-
|
|
325
|
-
So you can always override any setting from the deploy config, while UI edits made
|
|
326
|
-
to keys you don't set in the environment stay put. Clearing a setting in the UI
|
|
327
|
-
writes a tombstone, so a value you removed is not resurrected from the environment
|
|
328
|
-
on the next boot.
|
|
329
|
-
|
|
330
|
-
### Cloud Run + Cloud SQL (PostgreSQL)
|
|
331
|
-
|
|
332
|
-
Attach the Cloud SQL instance to the service (`--add-cloudsql-instances`) so the
|
|
333
|
-
Auth Proxy unix socket appears at `/cloudsql/<connection-name>`, then:
|
|
334
|
-
|
|
335
|
-
```bash
|
|
336
|
-
DB_DRIVER=postgres \
|
|
337
|
-
CLOUD_SQL_CONNECTION_NAME=my-project:us-central1:contextnest \
|
|
338
|
-
DB_USER=contextnest \
|
|
339
|
-
DB_PASSWORD=*** \
|
|
340
|
-
DB_NAME=contextnest \
|
|
341
|
-
DATA_ROOT=/mnt/vault \ # mount GCS/persistent storage here — see warning above
|
|
342
|
-
PROMPTOWL_KEY=pk_... \
|
|
343
|
-
node dist/index.js
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
Or with a plain TCP connection string (e.g. the Cloud SQL Proxy running on
|
|
347
|
-
localhost, or a private IP):
|
|
348
|
-
|
|
349
|
-
```bash
|
|
350
|
-
DATABASE_URL=postgres://contextnest:***@127.0.0.1:5432/contextnest \
|
|
351
|
-
DB_DRIVER=postgres \
|
|
352
|
-
DATA_ROOT=/mnt/vault \
|
|
353
|
-
npm start
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
## Typical deployments
|
|
357
|
-
|
|
358
|
-
### Local dev / single user
|
|
359
|
-
|
|
360
|
-
```bash
|
|
361
|
-
AUTH_MODE=open DATA_ROOT=./my-data npm run dev
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
Or the default dev mode if `npm run dev` already sets `AUTH_MODE=open` in your scripts.
|
|
365
|
-
|
|
366
|
-
### Team / multi-user behind a reverse proxy
|
|
367
|
-
|
|
368
|
-
```bash
|
|
369
|
-
AUTH_MODE=key \
|
|
370
|
-
CORS_ORIGINS="https://team.example.com,https://admin.example.com" \
|
|
371
|
-
DATA_ROOT=/var/lib/contextnest \
|
|
372
|
-
PROMPTOWL_KEY=pk_... \
|
|
373
|
-
npm start
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Terminate TLS at the proxy, forward `X-Forwarded-For` and `X-Real-IP` headers (the rate limiter reads them), and bind the server to `127.0.0.1` so only the proxy can reach it.
|
|
377
|
-
|
|
378
|
-
### Hosted / commercial SaaS
|
|
379
|
-
|
|
380
|
-
Same as team, plus:
|
|
381
|
-
- Run N instances behind a load balancer. SQLite + a shared filesystem is not a great long-term story for the metadata DB when scaling beyond one box — switch to the **PostgreSQL / Cloud SQL** backend (see [Database backends](#database-backends)) so all instances share one consistent database. The on-disk nest vault under `$DATA_ROOT/nests/` still needs durable shared storage.
|
|
382
|
-
- Set `TELEMETRY_ENABLED=true` and a valid `PROMPTOWL_KEY` so usage rolls up to PromptOwl.
|
|
383
|
-
- Rotate keys regularly via `DELETE /auth/keys/:id` + `POST /auth/keys`.
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## access.yaml (optional, open + key mode)
|
|
388
|
-
|
|
389
|
-
Drop an `access.yaml` at `$DATA_ROOT/access.yaml` to add a server-level ABAC layer — useful even in key mode for super-admins and group-based defaults.
|
|
390
|
-
|
|
391
|
-
```yaml
|
|
392
|
-
mode: restricted
|
|
393
|
-
allowed_users:
|
|
394
|
-
- "*.acme.com" # email wildcard — anyone @acme.com
|
|
395
|
-
- "partner@vendor.com" # exact match
|
|
396
|
-
super_admins:
|
|
397
|
-
- "ceo@acme.com" # admin on every nest: visibility, collaborators, stewards (not owner-only delete/transfer)
|
|
398
|
-
groups:
|
|
399
|
-
engineering:
|
|
400
|
-
default_permission: write
|
|
401
|
-
members:
|
|
402
|
-
- "*.eng.acme.com"
|
|
403
|
-
viewers:
|
|
404
|
-
default_permission: read
|
|
405
|
-
members:
|
|
406
|
-
- "*.contractor.acme.com"
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
See `STEWARDSHIP.md` for how super-admins and groups interact with per-nest stewardship.
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## Reload
|
|
414
|
-
|
|
415
|
-
Changing env vars requires a restart — the server reads them at boot. `tsx watch` in dev will pick up code changes automatically but not env changes.
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
All server configuration is driven by environment variables. Set them in your shell, in a `.env` file, in a Docker image, or via your process manager — however you deploy the rest of your stack.
|
|
4
|
+
|
|
5
|
+
## Auth modes
|
|
6
|
+
|
|
7
|
+
The server supports two authentication modes. You pick one per instance with `AUTH_MODE`.
|
|
8
|
+
|
|
9
|
+
### `AUTH_MODE=key` (default)
|
|
10
|
+
|
|
11
|
+
Every request requires an `Authorization: Bearer cnst_...` header. Use this for any multi-user or internet-facing deployment.
|
|
12
|
+
|
|
13
|
+
- Users register via `POST /auth/register` (email + password) or log in with PromptOwl via `POST /auth/device` → `POST /auth/promptowl`.
|
|
14
|
+
- API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use.
|
|
15
|
+
- Rate-limited login / register / device-auth endpoints (sliding window, per IP + per email).
|
|
16
|
+
- First PromptOwl-authenticated user becomes the server admin (atomic claim). Admin can invite teammates at `POST /auth/invite`.
|
|
17
|
+
|
|
18
|
+
Clients include the token on every request:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -H 'Authorization: Bearer cnst_<your-token>' http://your-server/nests
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### `AUTH_MODE=open`
|
|
25
|
+
|
|
26
|
+
No authentication required. Every request is attributed to the anonymous admin user (`00000000-...`). Everyone effectively owns every anon-created nest.
|
|
27
|
+
|
|
28
|
+
**Use this only when:**
|
|
29
|
+
- You're running locally for yourself (`bind 127.0.0.1`), or
|
|
30
|
+
- You're on a trusted LAN and don't care about access controls, or
|
|
31
|
+
- You're behind an upstream reverse proxy that already authenticates.
|
|
32
|
+
|
|
33
|
+
**Don't use this when:**
|
|
34
|
+
- The port is reachable from the public internet
|
|
35
|
+
- Multiple people share the deployment and need isolated data
|
|
36
|
+
- You need audit trails (every write shows up under "anonymous admin")
|
|
37
|
+
|
|
38
|
+
The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Full env var reference
|
|
43
|
+
|
|
44
|
+
| Var | Default | Purpose |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `PORT` | `3838` | HTTP port the server listens on. |
|
|
47
|
+
| `DATA_ROOT` | `./data` (relative to cwd) | Root directory for SQLite DB + nest filesystem vaults. Set to an absolute path in production so it's independent of where you `cd`'d from. |
|
|
48
|
+
| `DATABASE_PATH` | `$DATA_ROOT/community.db` | Override the SQLite file location explicitly. (SQLite backend only.) |
|
|
49
|
+
| `NEST_STORAGE_ROOT` | `$DATA_ROOT/nests` | Root directory for nest vault files (markdown + version history). Override to store nests separately from the DB — e.g. point it at a Cloud Storage (GCS) volume mount on Cloud Run (`/mnt/nests`) while the SQLite DB stays on `DATA_ROOT` off the mount. **Moving existing nests:** the app does not relocate files — copy `$DATA_ROOT/nests/*` to the new root (e.g. `gsutil rsync`) before switching, or existing nests will appear empty (DB rows present, files missing). On a GCS mount, also set `DRIFT_SCAN_INTERVAL_MS=0` to avoid constant FUSE scans, and run a single instance (GCS FUSE has no file locking — concurrent writers lose data). |
|
|
50
|
+
| `DB_DRIVER` | `sqlite` | Database backend: `sqlite` (default) or `postgres`. When unset, the server infers `postgres` if `DATABASE_URL` or `CLOUD_SQL_CONNECTION_NAME` is set, else `sqlite`. Existing deployments need no change — they stay on SQLite. See [Database backends](#database-backends). |
|
|
51
|
+
| `DATABASE_URL` | `""` | PostgreSQL connection string for a TCP connection, e.g. `postgres://user:pass@host:5432/dbname`. Setting it selects the Postgres backend (unless `DB_DRIVER=sqlite`). |
|
|
52
|
+
| `CLOUD_SQL_CONNECTION_NAME` | `""` | Cloud SQL instance connection name (`project:region:instance`). When set, the server connects over the Cloud SQL Auth Proxy unix socket at `/cloudsql/<name>` — the recommended Cloud Run setup. Combine with `DB_USER`, `DB_PASSWORD`, `DB_NAME`. |
|
|
53
|
+
| `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` | `localhost` / `5432` / `""` / `""` / `""` | Discrete Postgres connection params, used when neither `CLOUD_SQL_CONNECTION_NAME` nor `DATABASE_URL` is set. |
|
|
54
|
+
| `DB_POOL_MAX` | `10` | Max Postgres pool connections. Keep modest — Cloud SQL tiers cap total connections. |
|
|
55
|
+
| `DB_SSL` | `false` | Set `true` to require TLS on a Postgres TCP connection (not needed over the Cloud SQL unix socket, which is already secure). |
|
|
56
|
+
| `DB_SSL_CA` | `""` | Path to a CA certificate (PEM) for verify-ca/verify-full TLS when `DB_SSL=true`. |
|
|
57
|
+
| `AUTH_MODE` | `key` | `key` or `open`. See above. |
|
|
58
|
+
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
59
|
+
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
60
|
+
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
61
|
+
| `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
|
|
62
|
+
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Official deployment only — leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
|
|
63
|
+
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — only when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) |
|
|
64
|
+
| `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
|
|
65
|
+
| `OIDC_ISSUER` | `""` | OIDC issuer URL, e.g. `https://login.microsoftonline.com/<tenant>/v2.0` (Microsoft Entra ID) or `https://accounts.google.com` (Google). **`https://` only** — a non-https value is rejected with a warning and SSO stays off. Must serve `<issuer>/.well-known/openid-configuration`. |
|
|
66
|
+
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
|
67
|
+
| `OIDC_CLIENT_SECRET` | `""` | Client secret from your IdP app registration. Write-only on the Settings API — `GET /admin/settings` reports only `oidc_client_secret_set: true/false`, never the value. |
|
|
68
|
+
| `OIDC_ALLOWED_DOMAINS` | `""` (any) | Comma-separated email-domain allowlist, e.g. `acme.com, contractors.acme.com`. When set, only accounts whose asserted email is on a listed domain may sign in (others bounce with `domain_not_allowed`). Empty allows any domain the IdP asserts. |
|
|
69
|
+
| `OIDC_AUTO_PROVISION` | `true` | Create a user automatically on first successful OIDC sign-in (display name from the `name` claim). Set `false` to allow only pre-existing (invited/registered) users — unknown emails bounce with `not_invited`. |
|
|
70
|
+
| `OIDC_DEPARTMENT_TAGGING` | `false` | Auto-tag newly **created** documents with the creator's directory department: `dept:<slugified-department>` (lowercase, spaces → dashes, e.g. `dept:customer-success`) is appended to the document's tags, deduped against user-supplied tags. Applies on create only — never on update, never retroactively — and a user without a stored department is a silent no-op. The department is captured from the OIDC `department` ID-token claim on every SSO login (a login without the claim clears it, so directory moves propagate), so this is only meaningful when your IdP emits that claim — see [Department auto-tagging](#department-auto-tagging). Also editable from Settings → Single sign-on. |
|
|
71
|
+
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
|
|
72
|
+
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
73
|
+
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
74
|
+
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
75
|
+
| `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
|
|
76
|
+
| `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
|
|
77
|
+
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
|
|
78
|
+
| `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
|
|
79
|
+
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
|
|
80
|
+
| `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
|
|
81
|
+
| `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
|
|
82
|
+
| `TYPE_TABLE_ENABLED` | `true` | Same as above for **table** nodes. |
|
|
83
|
+
| `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
|
|
84
|
+
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from Settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
|
|
85
|
+
| `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
|
|
86
|
+
| `SUBAGENT_MAX_CHILDREN` | `16` | Max direct children a single run may spawn (clamped 1..128) — bounds the call tree's WIDTH. Together with `SUBAGENT_MAX_DEPTH` this caps total tree size so a runner can't fork-bomb the DB. |
|
|
87
|
+
| `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
|
|
88
|
+
| `RUN_MAX_CONCURRENT_ROOTS` | `50` | Max concurrently-running root (depth-0) runs per nest (clamped 1..1000). The subagent caps bound a single tree; this bounds how many trees can run at once, so triggers can't flood a nest. |
|
|
89
|
+
| `ANTHROPIC_API_KEY` | _(unset)_ | Server-wide default runner key for workflow-plane agent runs. Never returned by the API — the health endpoint reports presence only, and Settings shows a masked tail. |
|
|
90
|
+
| `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
|
|
91
|
+
| `MSTEAMS_WEBHOOK_URL` | _(unset — connector off)_ | Microsoft Teams incoming-webhook URL for the same governance events, posted as Adaptive Cards. `https://` only — the URL embeds a secret. Also editable from Settings in the UI. See [Microsoft Teams notifications](#microsoft-teams-notifications). |
|
|
92
|
+
| `SMTP_URL` | _(unset — connector off)_ | SMTP connection URL for email notifications (`smtp://` or `smtps://`, credentials inline). Requires `NOTIFY_EMAIL_FROM` and `NOTIFY_EMAIL_TO`. Also editable from Settings. |
|
|
93
|
+
| `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
|
|
94
|
+
| `NOTIFY_EMAIL_TO` | _(unset)_ | Comma-separated recipients for notification emails. |
|
|
95
|
+
| `NOTIFY_DEBOUNCE_MS` | `15000` | Per-nest buffer window before notifications flush; bursts collapse into one digest message. |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Microsoft Teams notifications
|
|
100
|
+
|
|
101
|
+
Set `MSTEAMS_WEBHOOK_URL` (or paste the URL into **Settings → Notifications →
|
|
102
|
+
Microsoft Teams notifications**) and the server posts the same governance
|
|
103
|
+
events the Slack connector covers — review requested / approved / rejected,
|
|
104
|
+
collaborator added, plus per-nest burst digests — to one Teams channel as
|
|
105
|
+
[Adaptive Cards](https://adaptivecards.io/). Leave it empty to turn the
|
|
106
|
+
connector off. Delivery is fire-and-forget with a short timeout: a Teams
|
|
107
|
+
outage or a bad URL never slows down or fails the action that triggered the
|
|
108
|
+
message.
|
|
109
|
+
|
|
110
|
+
> Naming note: the setting is `msteams_webhook_url` / `MSTEAMS_WEBHOOK_URL`
|
|
111
|
+
> because "teams" in this product means user groups (the `/teams` API), not
|
|
112
|
+
> Microsoft Teams.
|
|
113
|
+
|
|
114
|
+
### Create the incoming webhook in a Teams channel
|
|
115
|
+
|
|
116
|
+
Microsoft retired the classic Office 365 "Incoming Webhook" connectors;
|
|
117
|
+
webhooks are now created with the **Workflows** (Power Automate) app:
|
|
118
|
+
|
|
119
|
+
1. In Microsoft Teams, open the channel that should receive notifications,
|
|
120
|
+
click **⋯ (More options) → Workflows**.
|
|
121
|
+
2. Pick the template **"Post to a channel when a webhook request is
|
|
122
|
+
received"** (searching for "webhook" finds it), sign in if prompted, and
|
|
123
|
+
confirm the team + channel.
|
|
124
|
+
3. Create the flow and **copy the HTTP POST URL** it shows (a
|
|
125
|
+
`https://….logic.azure.com/…` or `https://….powerplatform.com/…` address).
|
|
126
|
+
4. Paste that URL into **Settings → Notifications → Microsoft Teams
|
|
127
|
+
notifications** (or set `MSTEAMS_WEBHOOK_URL`).
|
|
128
|
+
5. Save, then click **Send test** on the card to post a test message and
|
|
129
|
+
confirm the channel receives it. (The button tests the *saved* URL — save
|
|
130
|
+
first, then test. The Slack card has the same button.)
|
|
131
|
+
|
|
132
|
+
If your tenant still has a legacy Office 365 connector webhook, it is expected
|
|
133
|
+
to accept the same payload envelope (legacy connectors documented support for
|
|
134
|
+
Adaptive Card attachments, though we have not verified one end-to-end — the
|
|
135
|
+
card's plain-text `fallbackText` is the safety net) — but create new hooks via
|
|
136
|
+
Workflows.
|
|
137
|
+
|
|
138
|
+
The URL embeds a secret — anyone holding it can post to the channel, so treat
|
|
139
|
+
it like a password (it is why the setting requires `https://`). The server
|
|
140
|
+
sends the standard envelope Workflows expects:
|
|
141
|
+
`{"type": "message", "attachments": [{"contentType":
|
|
142
|
+
"application/vnd.microsoft.card.adaptive", "content": {…card…}}]}` with a
|
|
143
|
+
plain-text `fallbackText` for clients that cannot render cards.
|
|
144
|
+
|
|
145
|
+
Per-nest connector rows post the same Adaptive Card format to a nest-specific
|
|
146
|
+
webhook, with per-event filtering: `POST /nests/:id/connectors` with a JSON
|
|
147
|
+
body like `{"channel": "teams", "url": "https://…", "events":
|
|
148
|
+
["review_requested", "review_rejected"]}` (`events` may also be `["*"]`; the
|
|
149
|
+
`url` may be an `env:KEY` reference into the nest's env store). Manage rows
|
|
150
|
+
with `GET`/`PATCH`/`DELETE` on the same path.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Single sign-on (OIDC)
|
|
155
|
+
|
|
156
|
+
Generic OpenID Connect sign-in against any spec-compliant identity provider —
|
|
157
|
+
Microsoft Entra ID and Google are the first-class presets in **Settings →
|
|
158
|
+
Single sign-on**. The server runs a standard authorization-code flow with PKCE:
|
|
159
|
+
`GET /auth/oidc/login` redirects to your IdP, `GET /auth/oidc/callback` verifies
|
|
160
|
+
the returned ID token (issuer, audience, nonce, signature against the issuer's
|
|
161
|
+
JWKS) and starts a normal browser session. Users are looked up by email;
|
|
162
|
+
unknown emails are created automatically when `OIDC_AUTO_PROVISION` is on.
|
|
163
|
+
|
|
164
|
+
All seven `OIDC_*` values are also editable at runtime from **Settings → Single
|
|
165
|
+
sign-on** (superadmin only) — no restart needed. The Settings page shows the
|
|
166
|
+
exact **redirect URI** to register with your IdP:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
<PUBLIC_BASE_URL or server origin>/auth/oidc/callback
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Set `PUBLIC_BASE_URL` when the server sits behind a reverse proxy so the
|
|
173
|
+
redirect URI is derived from the canonical address rather than the incoming
|
|
174
|
+
Host header.
|
|
175
|
+
|
|
176
|
+
### Connect Microsoft Entra ID
|
|
177
|
+
|
|
178
|
+
1. **Register an app.** Entra admin center → *Identity → Applications → App
|
|
179
|
+
registrations → New registration*. Name it (e.g. "ContextNest"), leave the
|
|
180
|
+
account type at *Accounts in this organizational directory only*.
|
|
181
|
+
2. **Add the redirect URI.** In the registration: *Authentication → Add a
|
|
182
|
+
platform → Web*, and paste the redirect URI shown on the ContextNest
|
|
183
|
+
Settings page (`https://<your-server>/auth/oidc/callback`).
|
|
184
|
+
3. **Create a client secret.** *Certificates & secrets → New client secret*.
|
|
185
|
+
Copy the secret **value** (not the ID) immediately — it's shown once.
|
|
186
|
+
4. **Collect the IDs.** On the app's *Overview* page copy the **Application
|
|
187
|
+
(client) ID** and the **Directory (tenant) ID**.
|
|
188
|
+
5. **Fill in ContextNest.** Settings → Single sign-on: click **Microsoft Entra
|
|
189
|
+
ID**, paste the tenant ID when prompted (issuer becomes
|
|
190
|
+
`https://login.microsoftonline.com/<tenant>/v2.0`), then paste the client
|
|
191
|
+
ID and the secret value. Optionally restrict **Allowed email domains** to
|
|
192
|
+
your org's domain. Save, then flip the toggle **On** and save again (or in
|
|
193
|
+
the same save).
|
|
194
|
+
6. **Test.** Open the login page in a private window — a **Sign in with
|
|
195
|
+
Microsoft** button appears and round-trips through Entra. Entra accounts
|
|
196
|
+
without an `email` claim fall back to `preferred_username` (the UPN).
|
|
197
|
+
|
|
198
|
+
For **Google**: create an OAuth client ID (type *Web application*) in the
|
|
199
|
+
Google Cloud console, add the same redirect URI under *Authorized redirect
|
|
200
|
+
URIs*, and use the **Google** preset (issuer `https://accounts.google.com`)
|
|
201
|
+
with that client ID/secret.
|
|
202
|
+
|
|
203
|
+
### Department auto-tagging
|
|
204
|
+
|
|
205
|
+
With `OIDC_DEPARTMENT_TAGGING` on (Settings → Single sign-on → **Department
|
|
206
|
+
auto-tagging**), every document a user **creates** is tagged
|
|
207
|
+
`dept:<slugified-department>` from their directory department — e.g. a user in
|
|
208
|
+
*Customer Success* creates docs tagged `dept:customer-success`. Create-only:
|
|
209
|
+
edits never add or change the tag, and existing documents are never
|
|
210
|
+
retro-tagged. Users without a department (password accounts, or an IdP that
|
|
211
|
+
doesn't emit the claim) create untagged documents — never an error.
|
|
212
|
+
|
|
213
|
+
The department is read from the **`department` claim** in the OIDC ID token
|
|
214
|
+
and stored on the user at every SSO login: a new value updates it, an absent
|
|
215
|
+
claim clears it, so directory moves propagate on the user's next sign-in.
|
|
216
|
+
|
|
217
|
+
Microsoft Entra ID does **not** emit the claim by default — add it to the app
|
|
218
|
+
registration: *Token configuration → Add optional claim → Token type: **ID**
|
|
219
|
+
→ select **department** → Add* (grant the suggested Microsoft Graph
|
|
220
|
+
permission if prompted), and make sure the users' *Department* field is
|
|
221
|
+
populated in Entra. Other IdPs work too as long as they emit a string
|
|
222
|
+
`department` claim in the ID token (e.g. a Keycloak user-attribute mapper).
|
|
223
|
+
|
|
224
|
+
> **Privacy note:** the `dept:<slug>` tag becomes part of the document's
|
|
225
|
+
> visible metadata — anyone who can read the document (collaborators, shared
|
|
226
|
+
> nests, public nests) can see the creator's directory department. That's
|
|
227
|
+
> PII-adjacent organizational data; consider this before enabling on servers
|
|
228
|
+
> where documents are shared beyond the creator's own team or made public.
|
|
229
|
+
|
|
230
|
+
### Revoking access (offboarding)
|
|
231
|
+
|
|
232
|
+
Disabling a user **at the IdP** only blocks *new* sign-ins — an existing
|
|
233
|
+
ContextNest session stays valid until it expires (30 days). To cut access
|
|
234
|
+
immediately, **remove the user in ContextNest** (Settings → users): that wipes
|
|
235
|
+
all of their active sessions at once. Real offboarding is therefore two steps —
|
|
236
|
+
disable at the IdP *and* remove in ContextNest — with the ContextNest step being
|
|
237
|
+
the one that ends live sessions.
|
|
238
|
+
|
|
239
|
+
When a user signs themselves out, ContextNest performs an **RP-initiated
|
|
240
|
+
logout**: if the IdP advertises an `end_session_endpoint` (Entra, Okta,
|
|
241
|
+
Keycloak do; Google does not), the browser is bounced through it so the IdP
|
|
242
|
+
session ends too and the next "Sign in with SSO" click doesn't silently
|
|
243
|
+
re-authenticate. Where the IdP has no logout endpoint, the local session is
|
|
244
|
+
cleared and the user lands back on the login page (the IdP session persists —
|
|
245
|
+
that's the IdP's own timeout to manage).
|
|
246
|
+
|
|
247
|
+
### Email-verification trust assumption
|
|
248
|
+
|
|
249
|
+
An ID token whose `email_verified` claim is **explicitly `false`** is refused
|
|
250
|
+
(`?sso_error=email_not_verified`). When the claim is **absent**, the server
|
|
251
|
+
trusts the email claim — Microsoft Entra ID v2 tokens frequently omit it, and
|
|
252
|
+
both Entra and Google guarantee address ownership, so refusing on absence
|
|
253
|
+
would break the primary providers. This is a deliberate trust assumption:
|
|
254
|
+
**when your IdP allows unverified self-registered emails (e.g. an open
|
|
255
|
+
Keycloak realm), configure `oidc_allowed_domains` and disable self-registration
|
|
256
|
+
at the IdP** — otherwise anyone able to assert an arbitrary email at your IdP
|
|
257
|
+
could sign in as the matching local account.
|
|
258
|
+
|
|
259
|
+
Sign-in failures bounce back to the app as `/?sso_error=<code>` and surface as
|
|
260
|
+
a toast; codes: `disabled`, `not_configured`, `discovery_failed`,
|
|
261
|
+
`provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`,
|
|
262
|
+
`email_not_verified`, `domain_not_allowed`, `not_invited`, `rate_limited`,
|
|
263
|
+
`service_error`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Database backends
|
|
268
|
+
|
|
269
|
+
The governance/auth metadata (users, sessions, nests registry, stewards, reviews,
|
|
270
|
+
versions index, comments, telemetry) is stored in a relational database. Two
|
|
271
|
+
backends are supported:
|
|
272
|
+
|
|
273
|
+
- **SQLite (default).** A single file at `DATABASE_PATH`. Zero-config, ideal for
|
|
274
|
+
single-box and local deployments. Nothing changes for existing installs.
|
|
275
|
+
- **PostgreSQL / Cloud SQL.** For deployments where local disk isn't durable
|
|
276
|
+
(e.g. Google Cloud Run, where the container filesystem is ephemeral). Selected
|
|
277
|
+
by setting `DB_DRIVER=postgres`, `DATABASE_URL`, or `CLOUD_SQL_CONNECTION_NAME`.
|
|
278
|
+
|
|
279
|
+
On first connect the server creates its schema automatically (idempotent) — no
|
|
280
|
+
manual migration step. There is no built-in SQLite→Postgres data migration; a new
|
|
281
|
+
Postgres backend starts empty.
|
|
282
|
+
|
|
283
|
+
### First-boot upgrade passes
|
|
284
|
+
|
|
285
|
+
Some upgrades carry a one-shot data pass that runs at startup, before the server
|
|
286
|
+
accepts requests, and records itself in `schema_migrations` so it never repeats.
|
|
287
|
+
Two exist today: the `node_versions` backfill from `history.yaml`, and the
|
|
288
|
+
version-chain repair added in 1.13 (re-anchors documents whose version chain was
|
|
289
|
+
grafted by an older import, and moves inline patches into `v{N}.diff` files).
|
|
290
|
+
|
|
291
|
+
Both walk every document in every nest, so on a large vault the **first** boot
|
|
292
|
+
after upgrading can take noticeably longer than usual — reads of the on-disk
|
|
293
|
+
vault, not database work, so an install on network/FUSE storage feels it most.
|
|
294
|
+
Every subsequent boot skips the pass entirely. Progress and a per-pass summary
|
|
295
|
+
are printed to the server log.
|
|
296
|
+
|
|
297
|
+
> ⚠️ **Cloud Run durability — read this.** Cloud SQL persists only the *metadata
|
|
298
|
+
> database*. The actual **document content and version history** live on the
|
|
299
|
+
> filesystem under `$DATA_ROOT/nests/`, which on Cloud Run is **ephemeral** and is
|
|
300
|
+
> lost when an instance is recycled. Using Cloud SQL alone does **not** make a
|
|
301
|
+
> Cloud Run deployment fully durable. You must also put `$DATA_ROOT` on persistent,
|
|
302
|
+
> shared storage — e.g. mount a GCS bucket via Cloud Storage FUSE, or use a
|
|
303
|
+
> persistent volume — otherwise documents will disappear on redeploy/scale-in.
|
|
304
|
+
|
|
305
|
+
### Runtime settings persistence
|
|
306
|
+
|
|
307
|
+
Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
|
|
308
|
+
sign-in gate, logo, base URL, upload limit, feature flags, OIDC SSO, Slack/Teams/SMTP connectors)
|
|
309
|
+
plus the **installed license key** — are stored in the database (`server_settings`
|
|
310
|
+
table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
|
|
311
|
+
filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
|
|
312
|
+
horizontally-scaled instance had its own filesystem, so a change on one never
|
|
313
|
+
reached the others. The database (Cloud SQL in production) is durable **and** shared,
|
|
314
|
+
so a UI change persists across rebuilds and is seen by every instance. Rows are
|
|
315
|
+
loaded into the process environment at boot.
|
|
316
|
+
|
|
317
|
+
**Precedence between the deploy environment and a UI change.** Each stored row
|
|
318
|
+
remembers the deploy-env value in effect when it was written. At boot:
|
|
319
|
+
|
|
320
|
+
- If the deploy env value for that key is **unchanged**, the stored UI change wins
|
|
321
|
+
(your Settings-page edit survives an identical rebuild).
|
|
322
|
+
- If the deploy env value **changed** (you redeployed with a new value for that
|
|
323
|
+
variable), the deploy env wins and the stored row is re-synced to it.
|
|
324
|
+
|
|
325
|
+
So you can always override any setting from the deploy config, while UI edits made
|
|
326
|
+
to keys you don't set in the environment stay put. Clearing a setting in the UI
|
|
327
|
+
writes a tombstone, so a value you removed is not resurrected from the environment
|
|
328
|
+
on the next boot.
|
|
329
|
+
|
|
330
|
+
### Cloud Run + Cloud SQL (PostgreSQL)
|
|
331
|
+
|
|
332
|
+
Attach the Cloud SQL instance to the service (`--add-cloudsql-instances`) so the
|
|
333
|
+
Auth Proxy unix socket appears at `/cloudsql/<connection-name>`, then:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
DB_DRIVER=postgres \
|
|
337
|
+
CLOUD_SQL_CONNECTION_NAME=my-project:us-central1:contextnest \
|
|
338
|
+
DB_USER=contextnest \
|
|
339
|
+
DB_PASSWORD=*** \
|
|
340
|
+
DB_NAME=contextnest \
|
|
341
|
+
DATA_ROOT=/mnt/vault \ # mount GCS/persistent storage here — see warning above
|
|
342
|
+
PROMPTOWL_KEY=pk_... \
|
|
343
|
+
node dist/index.js
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Or with a plain TCP connection string (e.g. the Cloud SQL Proxy running on
|
|
347
|
+
localhost, or a private IP):
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
DATABASE_URL=postgres://contextnest:***@127.0.0.1:5432/contextnest \
|
|
351
|
+
DB_DRIVER=postgres \
|
|
352
|
+
DATA_ROOT=/mnt/vault \
|
|
353
|
+
npm start
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Typical deployments
|
|
357
|
+
|
|
358
|
+
### Local dev / single user
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
AUTH_MODE=open DATA_ROOT=./my-data npm run dev
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Or the default dev mode if `npm run dev` already sets `AUTH_MODE=open` in your scripts.
|
|
365
|
+
|
|
366
|
+
### Team / multi-user behind a reverse proxy
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
AUTH_MODE=key \
|
|
370
|
+
CORS_ORIGINS="https://team.example.com,https://admin.example.com" \
|
|
371
|
+
DATA_ROOT=/var/lib/contextnest \
|
|
372
|
+
PROMPTOWL_KEY=pk_... \
|
|
373
|
+
npm start
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Terminate TLS at the proxy, forward `X-Forwarded-For` and `X-Real-IP` headers (the rate limiter reads them), and bind the server to `127.0.0.1` so only the proxy can reach it.
|
|
377
|
+
|
|
378
|
+
### Hosted / commercial SaaS
|
|
379
|
+
|
|
380
|
+
Same as team, plus:
|
|
381
|
+
- Run N instances behind a load balancer. SQLite + a shared filesystem is not a great long-term story for the metadata DB when scaling beyond one box — switch to the **PostgreSQL / Cloud SQL** backend (see [Database backends](#database-backends)) so all instances share one consistent database. The on-disk nest vault under `$DATA_ROOT/nests/` still needs durable shared storage.
|
|
382
|
+
- Set `TELEMETRY_ENABLED=true` and a valid `PROMPTOWL_KEY` so usage rolls up to PromptOwl.
|
|
383
|
+
- Rotate keys regularly via `DELETE /auth/keys/:id` + `POST /auth/keys`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## access.yaml (optional, open + key mode)
|
|
388
|
+
|
|
389
|
+
Drop an `access.yaml` at `$DATA_ROOT/access.yaml` to add a server-level ABAC layer — useful even in key mode for super-admins and group-based defaults.
|
|
390
|
+
|
|
391
|
+
```yaml
|
|
392
|
+
mode: restricted
|
|
393
|
+
allowed_users:
|
|
394
|
+
- "*.acme.com" # email wildcard — anyone @acme.com
|
|
395
|
+
- "partner@vendor.com" # exact match
|
|
396
|
+
super_admins:
|
|
397
|
+
- "ceo@acme.com" # admin on every nest: visibility, collaborators, stewards (not owner-only delete/transfer)
|
|
398
|
+
groups:
|
|
399
|
+
engineering:
|
|
400
|
+
default_permission: write
|
|
401
|
+
members:
|
|
402
|
+
- "*.eng.acme.com"
|
|
403
|
+
viewers:
|
|
404
|
+
default_permission: read
|
|
405
|
+
members:
|
|
406
|
+
- "*.contractor.acme.com"
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
See `STEWARDSHIP.md` for how super-admins and groups interact with per-nest stewardship.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Reload
|
|
414
|
+
|
|
415
|
+
Changing env vars requires a restart — the server reads them at boot. `tsx watch` in dev will pick up code changes automatically but not env changes.
|