appilot-mcp 0.1.1 → 0.3.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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/LICENSE +15 -0
  4. package/README.md +102 -24
  5. package/dist/appilot-configurator.mcpb +0 -0
  6. package/dist/cli.d.ts +34 -0
  7. package/dist/cli.js +171 -0
  8. package/dist/client.d.ts +122 -3
  9. package/dist/client.js +306 -31
  10. package/dist/config.d.ts +14 -0
  11. package/dist/config.js +19 -0
  12. package/dist/contract/bundleSnapshot.js +8 -1
  13. package/dist/contract/healthContract.d.ts +1 -1
  14. package/dist/contract/healthContract.js +100 -10
  15. package/dist/contract/types.d.ts +37 -1
  16. package/dist/index.bundle.js +4487 -16520
  17. package/dist/index.js +7 -0
  18. package/dist/inspect.d.ts +88 -0
  19. package/dist/inspect.js +384 -0
  20. package/dist/manifest.d.ts +14 -2
  21. package/dist/manifest.js +31 -9
  22. package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
  23. package/dist/public-marketplace/README.md +23 -0
  24. package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
  25. package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
  26. package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
  27. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
  28. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
  29. package/dist/redaction.d.ts +18 -3
  30. package/dist/redaction.js +27 -3
  31. package/dist/remote/consent.d.ts +30 -20
  32. package/dist/remote/consent.js +114 -82
  33. package/dist/remote/consentMessages.d.ts +65 -0
  34. package/dist/remote/consentMessages.js +199 -0
  35. package/dist/remote/handoff.d.ts +10 -0
  36. package/dist/remote/handoff.js +44 -0
  37. package/dist/remote/httpServer.js +28 -5
  38. package/dist/remote/oauth.d.ts +39 -6
  39. package/dist/remote/oauth.js +281 -36
  40. package/dist/scaffold.d.ts +110 -1
  41. package/dist/scaffold.js +474 -39
  42. package/dist/server.js +425 -38
  43. package/dist/soak.js +21 -1
  44. package/dist/templates.d.ts +62 -0
  45. package/dist/templates.js +255 -0
  46. package/dist/verify.js +18 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/examples/app.appilot.json +212 -0
  50. package/mcpb/manifest.json +117 -15
  51. package/package.json +5 -3
  52. package/skills/app-configurator/SKILL.md +136 -25
@@ -2,9 +2,9 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "appilot-configurator",
4
4
  "display_name": "Appilot Configurator",
5
- "version": "0.1.1",
6
- "description": "Configure, audit, repair, back up, and restore an Appilot app's content model.",
7
- "long_description": "Connects Claude Desktop to an Appilot instance, cloud or on-premise, so it can read the app's content-model configuration, audit it against the Appilot config health contract, repair what it finds, and export or restore a whole configuration bundle. The static gate runs locally, so an audit works without network access to anything but your own instance. Access is a scoped service token you create in the Backoffice and revoke there.",
5
+ "version": "0.3.0",
6
+ "description": "Set up, configure, audit, repair, back up and restore an Appilot app's content model, and provision the app, its domains and its widget key.",
7
+ "long_description": "Connects Claude Desktop to an Appilot instance, cloud or on-premise. It provisions an app, its domains and its widget key, generates the host application's integration code, and verifies the result against the running page. It then reads the app's content-model configuration, audits it against the Appilot config health contract, repairs what it finds, and exports or restores a whole configuration bundle. The static gate runs locally, so an audit works without network access to anything but your own instance. Access is a scoped service token you create in the Backoffice and revoke there.",
8
8
  "author": {
9
9
  "name": "Appilot",
10
10
  "url": "https://appilot.space"
@@ -12,13 +12,20 @@
12
12
  "homepage": "https://appilot.space",
13
13
  "documentation": "https://docs.appilot.space/docs/developers/configure-with-ai/overview",
14
14
  "license": "ISC",
15
- "keywords": ["appilot", "configuration", "content-model", "audit"],
15
+ "keywords": [
16
+ "appilot",
17
+ "configuration",
18
+ "content-model",
19
+ "audit"
20
+ ],
16
21
  "server": {
17
22
  "type": "node",
18
23
  "entry_point": "dist/index.bundle.js",
19
24
  "mcp_config": {
20
25
  "command": "node",
21
- "args": ["${__dirname}/dist/index.bundle.js"],
26
+ "args": [
27
+ "${__dirname}/dist/index.bundle.js"
28
+ ],
22
29
  "env": {
23
30
  "APPILOT_BASE_URL": "${user_config.base_url}",
24
31
  "APPILOT_PAT": "${user_config.pat}",
@@ -27,15 +34,106 @@
27
34
  }
28
35
  },
29
36
  "tools": [
30
- { "name": "capabilities", "description": "Probe the instance for its version, migration level, and configurable entities." },
31
- { "name": "read_config", "description": "Read an app's content-model configuration as a normalized snapshot." },
32
- { "name": "validate_config", "description": "Audit a configuration against the Appilot config health contract." },
33
- { "name": "update_action_plan", "description": "Patch a stored action plan." },
34
- { "name": "update_control", "description": "Patch a control, for example to replace an unstable selector." },
35
- { "name": "update_knowledge", "description": "Patch a knowledge article." },
36
- { "name": "export_config", "description": "Export the whole configuration as a portable ConfigBundle." },
37
- { "name": "import_config", "description": "Import a ConfigBundle, dry-run first, merge or replace." },
38
- { "name": "soak_selectors", "description": "Check control selectors against a live page. Needs Playwright." }
37
+ {
38
+ "name": "capabilities",
39
+ "description": "Probe the instance for its version, migration level, and the closed vocabularies its entities accept."
40
+ },
41
+ {
42
+ "name": "read_config",
43
+ "description": "Read an app's content-model configuration as a normalized snapshot."
44
+ },
45
+ {
46
+ "name": "validate_config",
47
+ "description": "Audit a configuration against the Appilot config health contract."
48
+ },
49
+ {
50
+ "name": "entity_template",
51
+ "description": "A valid skeleton for one entity kind, with the enums this instance accepts."
52
+ },
53
+ {
54
+ "name": "create_entity",
55
+ "description": "Create a view, control, form, tool, zone, action plan, knowledge article or session template."
56
+ },
57
+ {
58
+ "name": "update_entity",
59
+ "description": "Patch any of the eight entity kinds by its row id."
60
+ },
61
+ {
62
+ "name": "delete_entity",
63
+ "description": "Delete an entity, refused when something still references it."
64
+ },
65
+ {
66
+ "name": "validate_action_plan",
67
+ "description": "Check draft plan sections before writing them."
68
+ },
69
+ {
70
+ "name": "export_config",
71
+ "description": "Export the whole configuration as a portable ConfigBundle."
72
+ },
73
+ {
74
+ "name": "import_config",
75
+ "description": "Import a ConfigBundle, dry-run first, merge or replace."
76
+ },
77
+ {
78
+ "name": "whoami",
79
+ "description": "Which organization, which app and which scopes this credential reaches."
80
+ },
81
+ {
82
+ "name": "create_app",
83
+ "description": "Provision an app, its domains and a widget key, with the snippets to paste in."
84
+ },
85
+ {
86
+ "name": "list_apps",
87
+ "description": "The apps this organization has provisioned, with their domains and verification status."
88
+ },
89
+ {
90
+ "name": "list_widget_keys",
91
+ "description": "The organization's widget keys by label and prefix. No raw key, no secret."
92
+ },
93
+ {
94
+ "name": "verify_domain",
95
+ "description": "Where a domain's DNS verification stands, and the TXT record it needs."
96
+ },
97
+ {
98
+ "name": "integration_snippet",
99
+ "description": "The script tag and boot call for an app that already exists. Writes nothing."
100
+ },
101
+ {
102
+ "name": "plan_manifest",
103
+ "description": "Preview what an appilot.app-manifest would change. Writes nothing."
104
+ },
105
+ {
106
+ "name": "apply_manifest",
107
+ "description": "Provision and configure an app from a previously planned manifest."
108
+ },
109
+ {
110
+ "name": "scaffold_integration",
111
+ "description": "The host application's integration source: identity relay, widget boot, a client action."
112
+ },
113
+ {
114
+ "name": "verify_integration",
115
+ "description": "Load the running page and report what is actually true about the integration."
116
+ },
117
+ {
118
+ "name": "soak_selectors",
119
+ "description": "Check control selectors against a live page. Needs Playwright."
120
+ },
121
+ {
122
+ "name": "inspect_page",
123
+ "description": "Read a page and rank locator candidates. Takes a URL or pasted markup."
124
+ },
125
+ {
126
+ "name": "scaffold_agent_first",
127
+ "description": "The artifacts one capability needs to be agent-operable."
128
+ },
129
+ {
130
+ "name": "report_feedback",
131
+ "description": "Report a gap or a defect in Appilot. Needs the feedback:write scope."
132
+ },
133
+ {
134
+ "name": "list_feedback",
135
+ "description": "This organization's reports, with status and occurrence count."
136
+ }
39
137
  ],
40
138
  "user_config": {
41
139
  "base_url": {
@@ -59,7 +157,11 @@
59
157
  }
60
158
  },
61
159
  "compatibility": {
62
- "platforms": ["darwin", "win32", "linux"],
160
+ "platforms": [
161
+ "darwin",
162
+ "win32",
163
+ "linux"
164
+ ],
63
165
  "runtimes": {
64
166
  "node": ">=18.0.0"
65
167
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appilot-mcp",
3
- "version": "0.1.1",
4
- "description": "Appilot MCP server: read, validate, and fix an Appilot app's content-model configuration against the config health contract. Endpoint-agnostic (cloud or on-premise), scoped-service-token auth, local static gate + optional live DOM soak.",
3
+ "version": "0.3.0",
4
+ "description": "Appilot MCP server: provision an Appilot app, scaffold and verify the host integration, then read, validate and fix its content-model configuration against the config health contract. Endpoint-agnostic (cloud or on-premise), scoped-service-token auth, local static gate + optional live DOM soak.",
5
5
  "homepage": "https://appilot.space",
6
6
  "author": "BetterKnow GmbH",
7
7
  "keywords": [
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "skills",
27
+ "examples",
27
28
  "mcpb",
28
29
  ".mcp.json",
29
30
  ".codex-plugin",
@@ -66,6 +67,7 @@
66
67
  "test": "vitest run",
67
68
  "test:watch": "vitest",
68
69
  "start:http": "node dist/index.bundle.js --http",
69
- "build:mcpb": "node scripts/build-mcpb.mjs"
70
+ "build:mcpb": "node scripts/build-mcpb.mjs",
71
+ "build:marketplace": "node scripts/build-public-marketplace.mjs"
70
72
  }
71
73
  }
@@ -15,10 +15,14 @@ well-formed configuration; every rule maps to a real failure mode.
15
15
 
16
16
  ## Two modes
17
17
 
18
- - **Connected (Appilot MCP server available).** Tools `capabilities`, `read_config`,
19
- `validate_config`, `update_action_plan` / `update_control` / `update_knowledge`,
20
- `export_config` / `import_config`, and `soak_selectors` let you read, validate,
21
- fix, back up, restore, and soak the user's live instance. Prefer this. Always
18
+ - **Connected (Appilot MCP server available).** `capabilities`, `read_config` and
19
+ `validate_config` read and audit. `entity_template`, `create_entity`,
20
+ `update_entity`, `delete_entity` and `validate_action_plan` author, across all
21
+ eight entity kinds. `whoami`, `list_apps`, `list_widget_keys`, `create_app`,
22
+ `verify_domain`, `scaffold_integration` and `integration_snippet` set an app up
23
+ from nothing. `inspect_page` looks at the screen. `export_config` /
24
+ `import_config` back up, clone and promote. `soak_selectors` and
25
+ `verify_integration` prove it works on the real page. Prefer this mode. Always
22
26
  start with `capabilities` so you configure against what THIS instance supports
23
27
  (on-premise instances trail cloud).
24
28
  - **Advisory (no MCP).** Ask the user to paste or export their config (or read it
@@ -36,21 +40,53 @@ runs in the customer's own repository, where you already are.
36
40
 
37
41
  1. **`whoami`.** Which org does this credential reach, and which scopes does it
38
42
  hold? A mistyped or revoked token should fail here, not as an opaque 401
39
- halfway through. Provisioning needs `provision:write`.
43
+ halfway through. Provisioning needs `provision:write`, minted in the
44
+ Backoffice under Service tokens with the preset called **Set up
45
+ integrations**. Only an organization administrator can mint it, so a
46
+ configurator who is not one has to ask for it rather than work around it.
47
+ `list_apps` then answers what already exists, and `list_widget_keys` answers
48
+ which keys were already minted, so a re-run recognises them instead of asking
49
+ for more.
40
50
  2. **`create_app`** with the app name and the domains it runs on. Idempotent, so
41
51
  a re-run converges rather than creating a second app. It returns the script
42
52
  tag, the boot snippet, and (once, at creation) the widget key and its secret.
53
+ A **live** key needs a verified domain: on a hostname nobody has proved they
54
+ control, the key comes back `blocked` with the TXT record to publish, while
55
+ the app and its domains are created anyway. Publish the record, then
56
+ `verify_domain` with `trigger`, or pass `isTest: true` for a test key that
57
+ claims nothing. The parameter is `isTest`, the same name the manifest and the
58
+ API use.
43
59
  The **key** is publishable and belongs in the page. The **secret** is not: it
44
60
  goes in the host's backend environment and must never reach a browser or a
45
61
  client bundle. If you are connected over the remote transport the secret is
46
62
  withheld on purpose; point the user at the Backoffice for it.
47
- 3. **`scaffold_integration`** for the host's framework. It returns file CONTENTS,
48
- which you write into the repository. The identity relay is the piece that
49
- matters: `resolveUser` is the security boundary and must derive the user from
50
- a credential the host already trusts, never from the request body.
63
+
64
+ **Working on the user's own machine.** There are two answers and they are not
65
+ equal. Preferred: have them run the app on a hostname that resolves to
66
+ 127.0.0.1 (`myapp.lvh.me:3000`, or an `/etc/hosts` entry) and register THAT
67
+ hostname as a domain. The page then resolves its tenant by hostname, no widget
68
+ key is needed, verification is not required for that path, and the turn gets
69
+ the full app context: views, controls, forms, plans, knowledge. Fallback:
70
+ bare `localhost` with a `wk_test_` key, which gets the organization and the
71
+ user and NO app context, because no domain resolves. Never pass `localhost` or
72
+ `127.0.0.1` as a domain: they name every developer's machine, one row for the
73
+ whole platform, and the write is refused.
74
+ 3. **`scaffold_integration`** for the host's framework, or `other` when the
75
+ backend is not Node (Django, Rails, PHP), which returns the raw exchange
76
+ instead. It returns file CONTENTS, which you write into the repository. The
77
+ identity relay is the piece that matters: `resolveUser` is the security
78
+ boundary and must derive the user from a credential the host already trusts,
79
+ never from the request body. The boot file reads the publishable key from the
80
+ framework's public variable (`NEXT_PUBLIC_APPILOT_WIDGET_KEY`,
81
+ `VITE_APPILOT_WIDGET_KEY`, `PUBLIC_APPILOT_WIDGET_KEY`); do not replace that
82
+ with a literal. `integration_snippet` returns the same two snippets later, for
83
+ an app that already exists, without a provisioning write.
51
84
  4. **Make one capability agent-operable** before adding screens' worth of
52
85
  configuration. The test for agent-first is whether a user could complete the
53
- feature end to end through the assistant alone.
86
+ feature end to end through the assistant alone. `scaffold_agent_first` takes a
87
+ `shape`: `create` for an open/fill/submit flow, `navigate` for one step to a
88
+ screen, and `read` for a capability the agent answers, which gets no action
89
+ plan at all.
54
90
  5. **`verify_integration`** against the running page. It answers what is actually
55
91
  true: does the domain resolve to a tenant, does the relay refuse an
56
92
  unauthenticated caller correctly, does the widget boot, is the console clean.
@@ -58,12 +94,23 @@ runs in the customer's own repository, where you already are.
58
94
  6. Then configure the content model, below, and `validate_config`.
59
95
 
60
96
  **A whole tenant as one file.** `plan_manifest` / `apply_manifest` take an
61
- `appilot.app-manifest`: the app, its domains, its keys, and a ConfigBundle. Keep
62
- it in the repository under version control, so configuration is diffable in a
63
- pull request and promotable from staging to production. `apply_manifest` requires
64
- the `planToken` that `plan_manifest` returned for the same file, so you cannot
65
- apply something nobody previewed, and an edit between the two calls fails rather
66
- than committing silently.
97
+ `appilot.app-manifest`: the app, its domains, its keys, and a ConfigBundle. A
98
+ complete example ships with the server at `examples/app.appilot.json`; copy it
99
+ rather than assembling one from doc snippets. Keep it in the repository under
100
+ version control, so configuration is diffable in a pull request and promotable
101
+ from staging to production. `apply_manifest` requires the `planToken` that
102
+ `plan_manifest` returned for the same file, so you cannot apply something nobody
103
+ previewed, and an edit between the two calls fails rather than committing
104
+ silently.
105
+
106
+ Two things about an apply are worth saying out loud. Only the CONFIG half is
107
+ revisioned: every commit persists a `pre_restore` revision, so a wrong import is
108
+ undone by restoring it, while a domain or a key the provisioning half created is
109
+ undone by hand. And provisioning is asked for by the manifest, not by the tool:
110
+ an apply whose app, domains and keys already exist needs only `config:write`,
111
+ which is what lets a CI token keep content in sync without being able to claim
112
+ domains. Outside an MCP client, `appilot-mcp plan <file>` and
113
+ `appilot-mcp apply <file>` are the same two calls from a shell.
67
114
 
68
115
  ## The content model (what you configure)
69
116
 
@@ -116,20 +163,82 @@ sparingly, over-authoring is an anti-pattern); the live DOM is ground truth.**
116
163
  as "Use when the user wants to…", localized. This is how the agent finds the
117
164
  plan; a weak/mislocalized description breaks adoption.
118
165
 
166
+ ## Authoring, one entity at a time
167
+
168
+ `entity_template(kind)` first, for any kind you have not written this session. It
169
+ returns a body with every container field present, the fields the write path
170
+ refuses without, and the closed enums THIS instance accepts. Two failures repeat
171
+ without it, and both are avoidable rather than diagnosable: a missing empty array
172
+ or object, which the import answers with a path you then have to decode; and a
173
+ guessed enum, because a brand-new app exports empty arrays and there was no
174
+ example to copy. Control locators are `id`, `class_text`, `aria`, `xpath`,
175
+ `semantic`. A CSS selector is not a kind, and a CSS selector list belongs under
176
+ `class_text`.
177
+
178
+ **Order is not a preference.** An entity that names another must come second.
179
+
180
+ 1. Controls, with stable locators.
181
+ 2. The form that names them as entry, submit and required fields.
182
+ 3. The tool, with its credential in the same call when it reaches an
183
+ authenticated backend. A tool with no stored credential runs in the page, so a
184
+ session template whose preflight calls it is refused.
185
+ 4. The action plan, after `validate_action_plan` on the draft sections.
186
+ 5. The knowledge article, carrying meaning and not steps.
187
+ 6. Activate what you created once `validate_config` is clean.
188
+
189
+ **Look at the page before you author a control.** `inspect_page` takes a `url`
190
+ and loads it, or `html` and scans markup the person pasted when there is no
191
+ browser on this connection. It ranks locator candidates by whether they survive
192
+ the next render and marks a framework-generated id as fragile, which is the
193
+ defect that passes every static check and then resolves to nothing. A scan of
194
+ pasted markup is not proof: run `soak_selectors` before you trust a locator.
195
+
196
+ **Building a capability, not just fixing one.** `scaffold_agent_first` returns
197
+ the four artifacts a capability needs so that a user could complete it through
198
+ the assistant alone: the tool, the client action when the operation belongs in
199
+ the page, the action plan that is the procedure, and the knowledge that carries
200
+ the meaning. That test, whether a user could finish end to end through the
201
+ assistant, is what agent-first means. A feature that only has a screen is not
202
+ done.
203
+
204
+ **A tool credential never travels over the remote transport.** `auth_secret` is
205
+ refused there, on purpose: a tool argument on that transport is already stored in
206
+ the conversation. Set it from a local stdio server, or in the Backoffice.
207
+
208
+ ## Reporting what is missing
209
+
210
+ `report_feedback` records a gap or a defect in Appilot itself, or in this
211
+ configuration. Say plainly what it does before you call it, because the honest
212
+ version is short: the report is recorded and it is read, and a reply is part of a
213
+ support plan rather than something promised here. The tool's answer says which of
214
+ those applies to this organization.
215
+
216
+ Call `list_feedback` first, so a known problem gets a counter rather than a
217
+ duplicate. Show the person the exact title and body before sending. Never put
218
+ configuration contents, knowledge bodies, customer data or a secret in a report;
219
+ the machine context that matters (server version, failing tool, error code) is
220
+ attached for you.
221
+
119
222
  ## Workflow
120
223
 
121
224
  1. **Discover.** Call `capabilities`. Note the version/migration level; degrade
122
225
  gracefully if a field is absent.
123
226
  2. **Read.** `read_config` for the target app. Understand the views, controls,
124
- forms, plans, and knowledge.
227
+ forms, tools, zones, plans, and knowledge. If the result carries a `gaps`
228
+ array, an entity could not be read: say so, and do not report on it. Every
229
+ lint over a gap passed by default, which makes the rest of the report an
230
+ incomplete audit, not a clean one.
125
231
  3. **Validate.** `validate_config`. It runs the contract locally and echoes the
126
232
  server-side plan trust boundary. Read the severity-ranked findings.
127
233
  4. **Explain.** Give the user a prioritized, plain-language report (critical →
128
234
  low), each finding with the concrete fix. Do not dump raw tool output.
129
- 5. **Fix (with consent).** Apply edits with `update_action_plan` /
130
- `update_control` / `update_knowledge`. The server re-validates the trust
131
- boundary and rejects an invalid write. For a broken create flow, that usually
132
- means: add a `Form` (entry + submit + required field) → author the plan as
235
+ 5. **Fix (with consent).** Patch with `update_entity`, using the `kind` and the
236
+ row `id` `read_config` returns. Create with `create_entity`, after
237
+ `entity_template` for that kind. Remove with `delete_entity`, which refuses
238
+ and names the dependents when something still points at the entity; prefer
239
+ `is_active: false` when you mean retire rather than remove. Do not export and
240
+ re-import a bundle to change one thing. For a broken create flow the fix is
241
+ usually: add a `Form` (entry + submit + required field) → author the plan as
133
242
  open → choose → `{{form:…}}` → submit → localize name/description/narratives →
134
243
  give the control a stable selector.
135
244
  6. **Re-validate.** `validate_config` again; confirm the findings are gone.
@@ -170,10 +279,12 @@ file is safe to save, commit, or share. `read_config` stays the reasoning view;
170
279
  ## Guardrails
171
280
 
172
281
  - Confirm before writing. Show the diff you intend to apply; get a yes.
173
- - Writing needs a `config:write` service token; reading/validating needs only
174
- `config:read`; provisioning needs `provision:write`. If a call 403s on scope,
175
- tell the user to mint a token with that scope in the Backoffice (admin →
176
- service tokens), don't work around it.
282
+ - Writing needs a `config:write` service token; reading and validating need only
283
+ `config:read`; provisioning needs `provision:write`. If a call is refused on
284
+ scope, say which scope is missing and who grants it: `provision:write` comes
285
+ from an organization administrator in the Backoffice, under Service tokens
286
+ with the "Set up integrations" preset, or from the account approval screen. Do
287
+ not work around it.
177
288
  - **Never put a widget secret in a file that ships to the browser.** It goes in
178
289
  the server environment: no `NEXT_PUBLIC_` prefix, no `VITE_`, no committed
179
290
  `.env`. The publishable key is different and belongs in the page.