@layers/amba-mcp 4.0.7 → 4.0.8
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/dist/index.js
CHANGED
|
@@ -5826,6 +5826,7 @@ function registerTools$18(server, apiClient) {
|
|
|
5826
5826
|
"Attach one exact custom hostname to one deployed function. The complete incoming path and query string are preserved when the hostname routes to the function.",
|
|
5827
5827
|
"MVP constraints: exact lowercase hostnames only; no wildcards or path rewrites. Attaching a subdomain does NOT create a `www` hostname automatically.",
|
|
5828
5828
|
"The response contains provider-neutral certificate/ownership status plus DNS validation instructions. Publish the returned CNAME and validation records, then call `amba_function_domains_refresh` until `live=true` (both statuses active and routing reconciliation successful).",
|
|
5829
|
+
"For every hostname, publish the real provider-stored CNAME and heed the unconditional `dns_note`. ALIAS, ANAME, and copied A/AAAA addresses do not preserve the relationship required for standard activation; use www or another subdomain if authoritative DNS cannot store an apex CNAME.",
|
|
5829
5830
|
"MVP limits use the effective project tier (free 1, pro 5, scale 20, enterprise or comped 50), with five attach attempts per project per hour. An unverified claim becomes eligible for exact-host reclaim after 24 hours; Worker route/KV allocation waits for active ownership.",
|
|
5830
5831
|
"Podcast/feed clients cannot supply an Amba API key. Their function must be deployed with `public: true`; validate any application-specific private token in the handler."
|
|
5831
5832
|
].join(" "), {
|
|
@@ -5972,7 +5973,8 @@ function registerTools$17(server, apiClient) {
|
|
|
5972
5973
|
registerTool(server, apiClient, "amba_sites_add_domain", [
|
|
5973
5974
|
"Attach a custom hostname to a site. The API registers the hostname with the upstream CDN provider server-side and persists the binding row with `cert_status=\"pending_validation\"`.",
|
|
5974
5975
|
"Response includes the DNS target (`<slug>.app.amba.host`) to set as a CNAME, plus any SSL validation records (DCV TXT/HTTP tokens) the customer must publish.",
|
|
5975
|
-
"
|
|
5976
|
+
"For every hostname, publish the real provider-stored CNAME and heed the unconditional `dns_note`; do not substitute ALIAS, ANAME, or copied A/AAAA addresses. Use www or another subdomain if authoritative DNS cannot store an apex CNAME.",
|
|
5977
|
+
"Poll `amba_sites_list_domains` until server-derived `live=true`. Certificate, hostname ownership, Worker route, host KV, and managed DNS (when applicable) are independent; never report success from provider status alone."
|
|
5976
5978
|
].join(" "), {
|
|
5977
5979
|
project_id: z.string().describe("The Amba project ID."),
|
|
5978
5980
|
name: z.string().describe("Site name to attach the domain to."),
|
|
@@ -5981,7 +5983,7 @@ function registerTools$17(server, apiClient) {
|
|
|
5981
5983
|
const normalisedHostname = hostname.toLowerCase();
|
|
5982
5984
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/sites/${encodeURIComponent(name)}/domains`, { body: { hostname: normalisedHostname } }));
|
|
5983
5985
|
}, ["amba_add_site_domain"]);
|
|
5984
|
-
registerTool(server, apiClient, "amba_sites_list_domains", "List custom hostnames attached to a site,
|
|
5986
|
+
registerTool(server, apiClient, "amba_sites_list_domains", "List custom hostnames attached to a site and re-poll readiness across certificate, ownership, Worker route, host KV, and managed DNS. Treat a hostname as ready only when server-derived `live=true`.", {
|
|
5985
5987
|
project_id: z.string().describe("The Amba project ID."),
|
|
5986
5988
|
name: z.string().describe("Site name.")
|
|
5987
5989
|
}, async ({ project_id, name }, { pat }) => {
|
|
@@ -6036,10 +6038,10 @@ function registerTools$16(server, apiClient) {
|
|
|
6036
6038
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/check`, { body: { domains: normalised } }));
|
|
6037
6039
|
});
|
|
6038
6040
|
registerTool(server, apiClient, "amba_domains_purchase", [
|
|
6039
|
-
"Buy a domain through Amba and
|
|
6041
|
+
"Buy a domain through Amba and create a custom-domain binding to one of the project's sites.",
|
|
6040
6042
|
"TWO-STEP / MONEY SAFETY: the first call (without `confirm`) returns a QUOTE with the price and `confirmation_required: true` and charges nothing. Surface the price to the user and get their go-ahead, then call again with `confirm: true` and `accept_price_usd` set to the quoted price to execute the purchase.",
|
|
6041
6043
|
"A real domain registration spends money and is permanent. If purchasing is not enabled on the deployment, the call returns the quote with `gated: true` and still charges nothing.",
|
|
6042
|
-
"
|
|
6044
|
+
"Registration and binding creation do not prove reachability. Report the public URL as live only when server-derived `hostname_live=true` (certificate, ownership, Worker route, host KV, and managed DNS are all reconciled); otherwise describe it as pending activation."
|
|
6043
6045
|
].join(" "), {
|
|
6044
6046
|
project_id: z.string().describe("The Amba project ID."),
|
|
6045
6047
|
domain: z.string().describe("The domain to buy, e.g. \"unbury.com\". Must be available (check first)."),
|
|
@@ -6061,7 +6063,7 @@ function registerTools$16(server, apiClient) {
|
|
|
6061
6063
|
if (years !== void 0) body.years = years;
|
|
6062
6064
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/purchase`, { body }));
|
|
6063
6065
|
});
|
|
6064
|
-
registerTool(server, apiClient, "amba_domains_list", "List domains this project has purchased through Amba, with registration status and
|
|
6066
|
+
registerTool(server, apiClient, "amba_domains_list", "List domains this project has purchased through Amba, with registration status, connected site, and authoritative hostname readiness. This call re-polls pending binding lifecycle; treat only `hostname_live=true` as reachable. Purchase/payment completion is reported separately by registration `status`.", {
|
|
6065
6067
|
project_id: z.string().describe("The Amba project ID."),
|
|
6066
6068
|
limit: z.number().int().min(1).max(200).optional().describe("Page size (1-200, default 50)."),
|
|
6067
6069
|
offset: z.number().int().min(0).optional().describe("Skip rows (default 0).")
|
|
@@ -9322,7 +9324,11 @@ function must be deployed with \`public: true\` and validate any private token
|
|
|
9322
9324
|
inside the handler. Effective-tier caps are free 1, pro 5, scale 20, and
|
|
9323
9325
|
enterprise/comped 50; attach is limited to five attempts per project per hour.
|
|
9324
9326
|
Unverified claims become eligible for reclaim after 24 hours, and routing
|
|
9325
|
-
resources are allocated only after ownership is active.
|
|
9327
|
+
resources are allocated only after ownership is active. Every hostname response
|
|
9328
|
+
includes an unconditional \`dns_note\`: publish a real provider-stored CNAME;
|
|
9329
|
+
ALIAS, ANAME, and copied A/AAAA addresses are not
|
|
9330
|
+
substitutes. Treat a hostname as ready only when \`live=true\`, never from TLS or
|
|
9331
|
+
\`cert_status=active\` alone.
|
|
9326
9332
|
|
|
9327
9333
|
### AI prompts
|
|
9328
9334
|
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
* Postgres tables; the hosting supplier (Neon) and the rest of the infra
|
|
15
15
|
* stack (Temporal / R2) stay scrubbed before exposing on the wire.
|
|
16
16
|
*/
|
|
17
|
-
export declare const AMBA_SETUP_INFRASTRUCTURE_MD = "# Infrastructure\n\nThe plumbing that sits behind every other surface: relational Postgres tables (Collections \u2014 schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Stripe / push credentials), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).\n\nIf gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.\n\n## MCP tools\n\n### Collections (relational Postgres tables)\n\nA collection is a relational Postgres table inside the project's isolated tenant database \u2014 typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients \u2014 admin tools bypass this.\n\nAdmin tools authenticate the developer/agent (pass `pat` or send it as the inbound Bearer) and take `project_id`. Client tools authenticate an end-user and take `api_key` (+ `session_token`) \u2014 NOT `project_id` and NOT a `pat`. Every row tool names the collection with `name`, never `collection`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_collections_create` | Create a typed collection. Pass `shared: true` for developer-seeded GLOBAL content (question banks, lookup tables) so `user_id` is nullable. | `{ project_id, name: \"todos\", columns: [{ name: \"title\", type: \"text\", nullable: false }, { name: \"done\", type: \"boolean\", nullable: false }, { name: \"due_at\", type: \"timestamptz\", nullable: true }], shared: false }` |\n| `amba_collections_list` | List collections in this project. | `{ project_id }` |\n| `amba_collections_get` | Read one collection's schema. | `{ project_id, name: \"todos\" }` |\n| `amba_collections_alter` | Exactly ONE of: `add_column`, `add_index`, `drop_column`, or `relax_user_id` per call. `relax_user_id: true` converts an existing collection to shared (drops the `user_id` NOT NULL). | `{ project_id, name: \"todos\", add_column: { name: \"priority\", type: \"integer\", nullable: true } }` |\n| `amba_collections_delete` | Drop the table (destructive). `confirm` must equal the collection name. | `{ project_id, name: \"todos\", confirm: \"todos\" }` |\n| `amba_admin_insert_row` | Insert one row as the developer (bypasses user-scope; `user_id` honored if present). | `{ project_id, name: \"todos\", row: { title: \"Sample\", done: false } }` |\n| `amba_admin_insert_rows` | Bulk-insert up to 500 rows in one atomic statement \u2014 the canonical seeding/migration path. `on_conflict`: `\"error\"` (default) or `\"skip\"`. | `{ project_id, name: \"questions\", rows: [{ q: \"...\" }, { q: \"...\" }], on_conflict: \"skip\" }` |\n| `amba_admin_list_rows` | Read rows as the developer. | `{ project_id, name: \"todos\", limit: 100 }` |\n| `amba_client_insert_row` | Insert as an end-user. Requires `api_key` (+ `session_token`). | `{ api_key, session_token, name: \"todos\", row: {...} }` |\n| `amba_client_list_rows` | Read as an end-user (auto user-scoped). | `{ api_key, session_token, name: \"todos\" }` |\n| `amba_client_get_row` | Get one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_update_row` | Update one row by id (end-user). Fields go in `set`. Omit `id` + pass `where` for a bulk update. | `{ api_key, session_token, name: \"todos\", id, set: {...} }` |\n| `amba_client_delete_row` | Soft-delete one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_count_rows` | Count rows matching an optional `where`. | `{ api_key, session_token, name: \"todos\", where: {...} }` |\n| `amba_client_find_rows` | Filter / sort / paginate rows (SDK-shaped `filter`). | `{ api_key, session_token, name: \"todos\", filter: {...}, order: [\"created_at desc\"], limit: 50 }` |\n| `amba_client_find_nearest_rows` | Vector-similarity search (rows with a `vector(<dim>)` column). | `{ api_key, session_token, name: \"todos\", column: \"embedding\", to_vector: [...], k: 10 }` |\n\nColumn types: `text`, `integer`, `bigint`, `numeric`, `boolean`, `timestamptz`, `date`, `jsonb`, `uuid`, `vector` (pass a separate `dimension` field, e.g. `{ name: \"embedding\", type: \"vector\", dimension: 1536 }` for OpenAI embeddings), plus array forms `text[]`, `integer[]`, `bigint[]`, `numeric[]`, `boolean[]`, `uuid[]`. Columns are NOT NULL unless `nullable: true`; column defaults are not supported (set values at insert time). Use `integer` (not `int`), `numeric` (not `float`/`real`/`double`), and `jsonb` (not `json`) \u2014 the validator rejects the aliases.\n\n### Functions (serverless code)\n\nRun user code in a sandbox triggered by HTTP, cron, or webhook. The function gets the tenant connection automatically via injected env.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_functions_deploy` | Deploy a function from source. | `{ project_id, name: \"send_welcome_email\", runtime: \"node22\", source: \"export default async (req) => { ... }\", trigger: { type: \"http\" } }` |\n| `amba_functions_list` | List functions. | `{ project_id }` |\n| `amba_functions_get` | Read function metadata. | `{ project_id, function_id }` |\n| `amba_functions_get_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |\n| `amba_functions_delete` | Delete a function. | `{ project_id, function_id }` |\n| `amba_functions_schedule` | Attach a cron schedule. | `{ project_id, function_id, cron: \"0 9 * * *\", timezone: \"America/Los_Angeles\" }` |\n| `amba_functions_pause_schedule` | Pause a scheduled trigger without deleting it. | `{ project_id, function_id }` |\n| `amba_functions_resume_schedule` | Resume. | `{ project_id, function_id }` |\n| `amba_functions_trigger_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |\n| `amba_function_domains_attach` | Attach one exact hostname to one function; returns DNS validation instructions. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_list` | List provider-neutral hostname, ownership, and certificate status. | `{ project_id, name: \"feed\" }` |\n| `amba_function_domains_refresh` | Re-poll DNS ownership and certificate state. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n\nFunction-domain routing preserves the complete incoming path and query string.\nIt is exact-host only (no wildcard/path rewrite and no automatic `www` for a\nsubdomain). Podcast/feed clients cannot attach an Amba API key, so their\nfunction must be deployed with `public: true` and validate any private token\ninside the handler. Effective-tier caps are free 1, pro 5, scale 20, and\nenterprise/comped 50; attach is limited to five attempts per project per hour.\nUnverified claims become eligible for reclaim after 24 hours, and routing\nresources are allocated only after ownership is active.\n\n### AI prompts\n\nManaged LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant \u2014 the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.\n\n**Two steps, in order:** first register the provider key with `amba_ai_providers_set`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with `provider_not_configured` (424) until the key is set.\n\n> The provider key is **not** a function secret. `amba_secrets_set` writes function-scoped Worker secrets, which the AI gateway never reads. Provider keys live in a separate gateway-owned store and are set **only** via `amba_ai_providers_set`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_ai_providers_set` | Register / rotate the upstream provider API key. **Do this first.** | `{ project_id, provider: \"anthropic\", api_key: \"sk-ant-...\" }` |\n| `amba_ai_providers_list` | List registered providers (`configured` = key set). | `{ project_id }` |\n| `amba_ai_providers_delete` | Revoke a provider key (fails if prompts still reference it). | `{ project_id, provider: \"anthropic\" }` |\n| `amba_ai_prompts_create` | Create a prompt template. `client_invokable: true` lets the device SDK invoke it directly. | `{ project_id, name: \"summarize\", provider: \"anthropic\", model: \"claude-opus-4-5\", system_prompt: \"Summarize the user's text in 2 sentences.\", client_invokable: true }` |\n| `amba_ai_prompts_list` | List prompts. | `{ project_id }` |\n| `amba_ai_prompts_get` | Read one prompt. | `{ project_id, name }` |\n| `amba_ai_prompts_update` | Edit a prompt (replaces all fields; bumps version). | `{ project_id, name, provider, model, system_prompt: \"...\" }` |\n| `amba_ai_prompts_invoke` | Invoke by name server-side (admin testing; works with `client_invokable: false`). Uses the named gateway path, so the prompt budget, rate limit, token cap, and spend attribution are enforced. | `{ project_id, name, messages: [{ role: \"user\", content: \"...\" }] }` |\n| `amba_ai_prompts_delete` | Delete. | `{ project_id, name }` |\n\n### Analytics + events + sessions\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_analytics_get` | Top-level metrics dashboard (MAU, DAU, retention). | `{ project_id, period: \"7d\" }` |\n| `amba_events_list` | Browse raw events. | `{ project_id, limit: 100, since: \"2026-05-19T00:00:00Z\" }` |\n| `amba_events_count` | Count events matching a filter. | `{ project_id, event: \"workout_completed\", since: \"...\" }` |\n| `amba_sessions_list` | List user sessions. | `{ project_id, limit: 50 }` |\n| `amba_sessions_analytics` | Session-level metrics. | `{ project_id, period: \"7d\" }` |\n| `amba_users_list_events` | Per-user event history. | `{ project_id, user_id }` |\n| `amba_users_export` | Export the full user list. | `{ project_id, format: \"csv\" }` |\n\n### Secrets + configs + integrations\n\nSecrets here become environment bindings on deployed functions. Omit `function`\nfor a project-wide secret or pass it to scope the value to one function. Setting\nor rotating a secret queues an asynchronous update for already-deployed\nfunctions; later deployments reconcile the binding too. They are NOT where AI\nprovider keys go (use `amba_ai_providers_set` for those \u2014 see AI prompts above).\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_secrets_set` | Set or rotate a function secret; omit `function` for project-wide scope or pass it for one function. Already-deployed functions receive it asynchronously. | `{ project_id, name: \"STRIPE_WEBHOOK_SECRET\", value: \"whsec_...\" }` |\n| `amba_secrets_get` | Read a secret (returns `\"<redacted>\"` unless explicitly requested). | `{ project_id, name }` |\n| `amba_secrets_list` | List secret names. | `{ project_id }` |\n| `amba_secrets_delete` | Delete. | `{ project_id, name }` |\n| `amba_configs_create` | Create a runtime config value (read from SDK as `Amba.config.fetch()`). | `{ project_id, key: \"primary_color\", value: \"#ff0066\", segment_id: null }` |\n| `amba_configs_list` | List configs. | `{ project_id }` |\n| `amba_configs_update` | Edit. | `{ project_id, config_id, value: \"...\" }` |\n| `amba_configs_delete` | Delete. | `{ project_id, config_id }` |\n| `amba_integrations_list` | List third-party integrations. | `{ project_id }` |\n| `amba_integrations_configure` | Configure a provider. | `{ project_id, provider: \"revenuecat\", config: { webhook_secret: \"...\", default_offering: \"...\" } }` |\n| `amba_integrations_set` | Set/replace integration config wholesale. | `{ project_id, provider, config }` |\n| `amba_integrations_patch` | Patch one field. | `{ project_id, provider, patch: { webhook_secret: \"...\" } }` |\n| `amba_integrations_test` | Send a test event to a configured provider. | `{ project_id, provider }` |\n\n### Media (file storage + CDN)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_media_upload` | Upload a file (returns a tenant-scoped URL). | `{ project_id, name: \"logo.png\", content_type: \"image/png\", data: \"<base64>\" }` |\n| `amba_media_list` | List files. | `{ project_id, folder: \"/\", limit: 100 }` |\n| `amba_media_delete` | Delete a file. | `{ project_id, file_id }` |\n| `amba_media_create_folder` | Create a logical folder. | `{ project_id, path: \"/uploads/avatars\" }` |\n| `amba_media_list_folders` | List folders. | `{ project_id }` |\n| `amba_media_delete_folder` | Delete a folder (must be empty). | `{ project_id, path }` |\n\n### Sites (static asset hosting)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_sites_deploy` | Deploy a static site bundle (zip / tar). | `{ project_id, name: \"marketing\", bundle: \"<base64>\", index: \"index.html\" }` |\n| `amba_sites_list` | List sites. | `{ project_id }` |\n| `amba_sites_get` | Read a site. | `{ project_id, site_id }` |\n| `amba_sites_add_domain` | Attach a custom domain. | `{ project_id, site_id, domain: \"marketing.example.com\" }` |\n| `amba_sites_list_domains` | List domains on a site. | `{ project_id, site_id }` |\n| `amba_sites_remove_domain` | Detach a domain. | `{ project_id, site_id, domain }` |\n| `amba_sites_delete` | Delete a site. | `{ project_id, site_id }` |\n\n### Purchased domains + email forwarding\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_domains_search` | Search available domains (free). | `{ project_id, query: \"myapp\" }` |\n| `amba_domains_check` | Check authoritative price + availability. | `{ project_id, domains: [\"myapp.com\"] }` |\n| `amba_domains_purchase` | Quote, then confirm, a domain purchase. | `{ project_id, domain: \"myapp.com\", site: \"marketing\" }` |\n| `amba_domains_list` | List purchased domains. | `{ project_id }` |\n| `amba_domains_email_enable` | Enable inbound routing when no MX conflict exists. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_destinations_add` | Add a destination mailbox; returns action-required until verified. | `{ project_id, domain: \"myapp.com\", email: \"owner@example.net\" }` |\n| `amba_domains_email_destinations_get` | Poll destination verification. | `{ project_id, domain: \"myapp.com\", destination_id }` |\n| `amba_domains_email_forwards_set` | Create/update a literal forward. | `{ project_id, domain: \"myapp.com\", source: \"support\", destination: \"owner@example.net\" }` |\n| `amba_domains_email_forwards_list` | List literal forwards. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_forwards_delete` | Delete a literal forward. | `{ project_id, domain: \"myapp.com\", forward_id }` |\n| `amba_domains_email_catch_all_set` | Enable/update/disable catch-all. | `{ project_id, domain: \"myapp.com\", enabled: true, destination: \"owner@example.net\" }` |\n| `amba_domains_email_catch_all_get` | Read catch-all state. | `{ project_id, domain: \"myapp.com\" }` |\n\n## SDK init per stack\n\n`Amba.configure(...)` runs first. The infrastructure surfaces \u2014 collections, AI, config, flags, events \u2014 are SDK-side reads; the snippets below show what the client calls look like.\n\n### Expo / React Native\n\n```tsx\nimport { Amba } from '@layers/amba-expo';\n\n// Collections \u2014 typed table, user-scoped reads + writes\ntype Todo = { id: string; title: string; done: boolean; created_at: string };\n\nconst { data: todos } = await Amba.collections.find<Todo>('todos', {\n filter: Amba.collections.where.eq('done', false),\n order: [{ column: 'created_at', direction: 'desc' }],\n limit: 50,\n});\n\nconst newTodo = await Amba.collections.insert('todos', { title: 'Ship the app', done: false });\nawait Amba.collections.update('todos', newTodo.id, { done: true });\nawait Amba.collections.delete('todos', newTodo.id);\n\n// AI \u2014 call a managed prompt (prompt_slug names the registered prompt)\nconst response = await Amba.ai.anthropic.messages.create({\n prompt_slug: 'summarize',\n variables: { text: 'A long article about backend services \u2026' },\n});\n\n// Track an analytics event\nawait Amba.events.track('button_clicked', { button: 'cta' });\n\n// Read runtime config\nconst config = await Amba.config.fetch();\n\n// Read a feature flag\nconst showBeta = await Amba.flags.get('beta_feature');\n\n// Diagnostics \u2014 wire-verify\nconst ping = await Amba.diagnostics.ping();\nif (!ping.ok) console.error('Amba misconfigured:', ping);\n```\n\n### Web\n\n```ts\nimport { Amba } from '@layers/amba-web';\n\nconst { data: todos } = await Amba.collections.find('todos', {\n filter: Amba.collections.where.eq('done', false),\n limit: 50,\n});\nawait Amba.collections.insert('todos', { title: 'Ship', done: false });\nawait Amba.events.track('page_view', { path: location.pathname });\n```\n\nWith `@layers/amba-react`:\n\n```tsx\nimport { useCollection, useFlag } from '@layers/amba-react';\n\nfunction TodoList() {\n const { data: todos, loading, refetch } = useCollection<{ id: string; title: string }>('todos');\n const showArchive = useFlag('archive_todos');\n if (loading) return <Spinner />;\n return (\n <ul>\n {todos?.map(t => <li key={t.id}>{t.title}</li>)}\n {showArchive && <ArchiveButton onArchive={refetch} />}\n </ul>\n );\n}\n```\n\n### iOS (Swift)\n\n```swift\nimport Amba\n\nstruct Todo: Codable {\n let id: String\n let title: String\n let done: Bool\n}\n\nlet response = try await Amba.collections.find(\"todos\", as: Todo.self)\n_ = try await Amba.collections.insert(\"todos\", row: [\"title\": \"Ship\", \"done\": false])\n\nlet config = try await Amba.config.fetch()\nlet showBeta = try await Amba.flags.get(name: \"beta_feature\")\ntry await Amba.events.track(\"app_opened\", properties: [\"source\": \"deep_link\"])\n\nlet reply = try await Amba.ai.anthropic.messages.create(\n request: AiMessageRequest(promptSlug: \"summarize\", variables: [\"text\": \"A long article...\"])\n)\n```\n\n### Android (Kotlin)\n\n```kotlin\ndata class Todo(val id: String, val title: String, val done: Boolean)\n\nval todos = Amba.collections.find<Todo>(\"todos\")\nAmba.collections.insert(\"todos\", mapOf(\"title\" to \"Ship\", \"done\" to false))\n\nval config = Amba.config.fetch()\nval showBeta = Amba.flags.get(\"beta_feature\")\nAmba.events.track(\"app_opened\", mapOf(\"source\" to \"deep_link\"))\n```\n\n### Flutter\n\n```dart\nimport 'package:amba/amba.dart';\n\nfinal response = await Amba.collections.find('todos', limit: 50);\nawait Amba.collections.insert('todos', {'title': 'Ship', 'done': false});\nfinal config = await Amba.config.fetch();\nfinal showBeta = await Amba.flags.get('beta_feature');\nawait Amba.events.track('app_opened', {'source': 'deep_link'});\n```\n\n## Common follow-ups\n\nBatch.\n\n1. **Custom data tables (collections):** any domain-specific tables to create?\n - Yes \u2014 I'll list them. (For each: name + columns + types.)\n - No, just use the canned Amba surfaces (auth, push, gamification, etc.)\n - Auto-create from the existing code's models \u2014 read `lib/models/`, `src/types/`, `Models/`, infer column lists, confirm with me.\n\n2. **Custom backend logic (functions):** any server-side code to deploy?\n - Yes \u2014 describe what it should do. (Then offer to scaffold a function template and deploy.)\n - No\n\n3. **AI features:** want managed LLM prompts?\n - Yes \u2014 what's the use case? (summarize, translate, classify, generate, custom)\n - No\n\n4. **Analytics:** which tracker do you want?\n - Only Amba's built-in events (recommended \u2014 already wired)\n - Amba + your own analytics pipeline (subscribe a webhook to project events via `amba_webhooks_create` and forward server-side)\n - None (rarely useful \u2014 events drive XP / achievements / streaks; disabling cripples gamification)\n\n5. **Third-party integrations to set up:**\n - [ ] RevenueCat (IAP / subscriptions on iOS + Android)\n - [ ] Superwall (paywall A/B)\n - [ ] Stripe Billing (web subscriptions through the app's own Stripe account \u2014 provider `stripe_billing`)\n - [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for `Amba.ai.*` \u2014 set via `amba_ai_providers_set`, **not** `amba_integrations_configure`)\n\n6. **Feature flags:** seed any starter flags?\n - Yes \u2014 wire `beta_feature` (off by default) so I can ship the wiring before the feature exists\n - No\n\n7. **Static site:** want a marketing page hosted under your tenant subdomain?\n - Yes \u2014 scaffold and deploy a 1-page index\n - No\n\n## Re-run behavior\n\n1. Before creating:\n - `amba_collections_list` \u2014 match on `name`. Collisions: never silently recreate (data loss). Offer `amba_collections_alter` to add new columns instead.\n - `amba_functions_list` \u2014 match on `name`. Collisions: ask to redeploy (with the new source) or skip.\n - `amba_ai_prompts_list` \u2014 match on `name`. Same. (And `amba_ai_providers_list` \u2014 match on `provider`; re-running `amba_ai_providers_set` rotates the key in place.)\n - `amba_integrations_list` \u2014 match on `provider`. Same.\n - `amba_configs_list` \u2014 match on `key`. Same.\n\n2. **Never call `amba_collections_delete` on re-run unless the user explicitly asks** \u2014 this drops the underlying table and every row in it across every user of the tenant.\n\n3. For functions: re-deploying replaces source in place (versioned server-side). It's safe to call `amba_functions_deploy` with the same name + new source.\n\n4. For integrations: if a provider is already configured, prefer `amba_integrations_patch` (partial update) over `amba_integrations_set` (full replace).\n\n5. Secrets: don't list secret values in chat output, even on read. Just confirm \"OPENAI_API_KEY is set\" / \"not set\".\n";
|
|
17
|
+
export declare const AMBA_SETUP_INFRASTRUCTURE_MD = "# Infrastructure\n\nThe plumbing that sits behind every other surface: relational Postgres tables (Collections \u2014 schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Stripe / push credentials), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).\n\nIf gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.\n\n## MCP tools\n\n### Collections (relational Postgres tables)\n\nA collection is a relational Postgres table inside the project's isolated tenant database \u2014 typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients \u2014 admin tools bypass this.\n\nAdmin tools authenticate the developer/agent (pass `pat` or send it as the inbound Bearer) and take `project_id`. Client tools authenticate an end-user and take `api_key` (+ `session_token`) \u2014 NOT `project_id` and NOT a `pat`. Every row tool names the collection with `name`, never `collection`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_collections_create` | Create a typed collection. Pass `shared: true` for developer-seeded GLOBAL content (question banks, lookup tables) so `user_id` is nullable. | `{ project_id, name: \"todos\", columns: [{ name: \"title\", type: \"text\", nullable: false }, { name: \"done\", type: \"boolean\", nullable: false }, { name: \"due_at\", type: \"timestamptz\", nullable: true }], shared: false }` |\n| `amba_collections_list` | List collections in this project. | `{ project_id }` |\n| `amba_collections_get` | Read one collection's schema. | `{ project_id, name: \"todos\" }` |\n| `amba_collections_alter` | Exactly ONE of: `add_column`, `add_index`, `drop_column`, or `relax_user_id` per call. `relax_user_id: true` converts an existing collection to shared (drops the `user_id` NOT NULL). | `{ project_id, name: \"todos\", add_column: { name: \"priority\", type: \"integer\", nullable: true } }` |\n| `amba_collections_delete` | Drop the table (destructive). `confirm` must equal the collection name. | `{ project_id, name: \"todos\", confirm: \"todos\" }` |\n| `amba_admin_insert_row` | Insert one row as the developer (bypasses user-scope; `user_id` honored if present). | `{ project_id, name: \"todos\", row: { title: \"Sample\", done: false } }` |\n| `amba_admin_insert_rows` | Bulk-insert up to 500 rows in one atomic statement \u2014 the canonical seeding/migration path. `on_conflict`: `\"error\"` (default) or `\"skip\"`. | `{ project_id, name: \"questions\", rows: [{ q: \"...\" }, { q: \"...\" }], on_conflict: \"skip\" }` |\n| `amba_admin_list_rows` | Read rows as the developer. | `{ project_id, name: \"todos\", limit: 100 }` |\n| `amba_client_insert_row` | Insert as an end-user. Requires `api_key` (+ `session_token`). | `{ api_key, session_token, name: \"todos\", row: {...} }` |\n| `amba_client_list_rows` | Read as an end-user (auto user-scoped). | `{ api_key, session_token, name: \"todos\" }` |\n| `amba_client_get_row` | Get one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_update_row` | Update one row by id (end-user). Fields go in `set`. Omit `id` + pass `where` for a bulk update. | `{ api_key, session_token, name: \"todos\", id, set: {...} }` |\n| `amba_client_delete_row` | Soft-delete one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_count_rows` | Count rows matching an optional `where`. | `{ api_key, session_token, name: \"todos\", where: {...} }` |\n| `amba_client_find_rows` | Filter / sort / paginate rows (SDK-shaped `filter`). | `{ api_key, session_token, name: \"todos\", filter: {...}, order: [\"created_at desc\"], limit: 50 }` |\n| `amba_client_find_nearest_rows` | Vector-similarity search (rows with a `vector(<dim>)` column). | `{ api_key, session_token, name: \"todos\", column: \"embedding\", to_vector: [...], k: 10 }` |\n\nColumn types: `text`, `integer`, `bigint`, `numeric`, `boolean`, `timestamptz`, `date`, `jsonb`, `uuid`, `vector` (pass a separate `dimension` field, e.g. `{ name: \"embedding\", type: \"vector\", dimension: 1536 }` for OpenAI embeddings), plus array forms `text[]`, `integer[]`, `bigint[]`, `numeric[]`, `boolean[]`, `uuid[]`. Columns are NOT NULL unless `nullable: true`; column defaults are not supported (set values at insert time). Use `integer` (not `int`), `numeric` (not `float`/`real`/`double`), and `jsonb` (not `json`) \u2014 the validator rejects the aliases.\n\n### Functions (serverless code)\n\nRun user code in a sandbox triggered by HTTP, cron, or webhook. The function gets the tenant connection automatically via injected env.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_functions_deploy` | Deploy a function from source. | `{ project_id, name: \"send_welcome_email\", runtime: \"node22\", source: \"export default async (req) => { ... }\", trigger: { type: \"http\" } }` |\n| `amba_functions_list` | List functions. | `{ project_id }` |\n| `amba_functions_get` | Read function metadata. | `{ project_id, function_id }` |\n| `amba_functions_get_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |\n| `amba_functions_delete` | Delete a function. | `{ project_id, function_id }` |\n| `amba_functions_schedule` | Attach a cron schedule. | `{ project_id, function_id, cron: \"0 9 * * *\", timezone: \"America/Los_Angeles\" }` |\n| `amba_functions_pause_schedule` | Pause a scheduled trigger without deleting it. | `{ project_id, function_id }` |\n| `amba_functions_resume_schedule` | Resume. | `{ project_id, function_id }` |\n| `amba_functions_trigger_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |\n| `amba_function_domains_attach` | Attach one exact hostname to one function; returns DNS validation instructions. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_list` | List provider-neutral hostname, ownership, and certificate status. | `{ project_id, name: \"feed\" }` |\n| `amba_function_domains_refresh` | Re-poll DNS ownership and certificate state. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n\nFunction-domain routing preserves the complete incoming path and query string.\nIt is exact-host only (no wildcard/path rewrite and no automatic `www` for a\nsubdomain). Podcast/feed clients cannot attach an Amba API key, so their\nfunction must be deployed with `public: true` and validate any private token\ninside the handler. Effective-tier caps are free 1, pro 5, scale 20, and\nenterprise/comped 50; attach is limited to five attempts per project per hour.\nUnverified claims become eligible for reclaim after 24 hours, and routing\nresources are allocated only after ownership is active. Every hostname response\nincludes an unconditional `dns_note`: publish a real provider-stored CNAME;\nALIAS, ANAME, and copied A/AAAA addresses are not\nsubstitutes. Treat a hostname as ready only when `live=true`, never from TLS or\n`cert_status=active` alone.\n\n### AI prompts\n\nManaged LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant \u2014 the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.\n\n**Two steps, in order:** first register the provider key with `amba_ai_providers_set`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with `provider_not_configured` (424) until the key is set.\n\n> The provider key is **not** a function secret. `amba_secrets_set` writes function-scoped Worker secrets, which the AI gateway never reads. Provider keys live in a separate gateway-owned store and are set **only** via `amba_ai_providers_set`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_ai_providers_set` | Register / rotate the upstream provider API key. **Do this first.** | `{ project_id, provider: \"anthropic\", api_key: \"sk-ant-...\" }` |\n| `amba_ai_providers_list` | List registered providers (`configured` = key set). | `{ project_id }` |\n| `amba_ai_providers_delete` | Revoke a provider key (fails if prompts still reference it). | `{ project_id, provider: \"anthropic\" }` |\n| `amba_ai_prompts_create` | Create a prompt template. `client_invokable: true` lets the device SDK invoke it directly. | `{ project_id, name: \"summarize\", provider: \"anthropic\", model: \"claude-opus-4-5\", system_prompt: \"Summarize the user's text in 2 sentences.\", client_invokable: true }` |\n| `amba_ai_prompts_list` | List prompts. | `{ project_id }` |\n| `amba_ai_prompts_get` | Read one prompt. | `{ project_id, name }` |\n| `amba_ai_prompts_update` | Edit a prompt (replaces all fields; bumps version). | `{ project_id, name, provider, model, system_prompt: \"...\" }` |\n| `amba_ai_prompts_invoke` | Invoke by name server-side (admin testing; works with `client_invokable: false`). Uses the named gateway path, so the prompt budget, rate limit, token cap, and spend attribution are enforced. | `{ project_id, name, messages: [{ role: \"user\", content: \"...\" }] }` |\n| `amba_ai_prompts_delete` | Delete. | `{ project_id, name }` |\n\n### Analytics + events + sessions\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_analytics_get` | Top-level metrics dashboard (MAU, DAU, retention). | `{ project_id, period: \"7d\" }` |\n| `amba_events_list` | Browse raw events. | `{ project_id, limit: 100, since: \"2026-05-19T00:00:00Z\" }` |\n| `amba_events_count` | Count events matching a filter. | `{ project_id, event: \"workout_completed\", since: \"...\" }` |\n| `amba_sessions_list` | List user sessions. | `{ project_id, limit: 50 }` |\n| `amba_sessions_analytics` | Session-level metrics. | `{ project_id, period: \"7d\" }` |\n| `amba_users_list_events` | Per-user event history. | `{ project_id, user_id }` |\n| `amba_users_export` | Export the full user list. | `{ project_id, format: \"csv\" }` |\n\n### Secrets + configs + integrations\n\nSecrets here become environment bindings on deployed functions. Omit `function`\nfor a project-wide secret or pass it to scope the value to one function. Setting\nor rotating a secret queues an asynchronous update for already-deployed\nfunctions; later deployments reconcile the binding too. They are NOT where AI\nprovider keys go (use `amba_ai_providers_set` for those \u2014 see AI prompts above).\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_secrets_set` | Set or rotate a function secret; omit `function` for project-wide scope or pass it for one function. Already-deployed functions receive it asynchronously. | `{ project_id, name: \"STRIPE_WEBHOOK_SECRET\", value: \"whsec_...\" }` |\n| `amba_secrets_get` | Read a secret (returns `\"<redacted>\"` unless explicitly requested). | `{ project_id, name }` |\n| `amba_secrets_list` | List secret names. | `{ project_id }` |\n| `amba_secrets_delete` | Delete. | `{ project_id, name }` |\n| `amba_configs_create` | Create a runtime config value (read from SDK as `Amba.config.fetch()`). | `{ project_id, key: \"primary_color\", value: \"#ff0066\", segment_id: null }` |\n| `amba_configs_list` | List configs. | `{ project_id }` |\n| `amba_configs_update` | Edit. | `{ project_id, config_id, value: \"...\" }` |\n| `amba_configs_delete` | Delete. | `{ project_id, config_id }` |\n| `amba_integrations_list` | List third-party integrations. | `{ project_id }` |\n| `amba_integrations_configure` | Configure a provider. | `{ project_id, provider: \"revenuecat\", config: { webhook_secret: \"...\", default_offering: \"...\" } }` |\n| `amba_integrations_set` | Set/replace integration config wholesale. | `{ project_id, provider, config }` |\n| `amba_integrations_patch` | Patch one field. | `{ project_id, provider, patch: { webhook_secret: \"...\" } }` |\n| `amba_integrations_test` | Send a test event to a configured provider. | `{ project_id, provider }` |\n\n### Media (file storage + CDN)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_media_upload` | Upload a file (returns a tenant-scoped URL). | `{ project_id, name: \"logo.png\", content_type: \"image/png\", data: \"<base64>\" }` |\n| `amba_media_list` | List files. | `{ project_id, folder: \"/\", limit: 100 }` |\n| `amba_media_delete` | Delete a file. | `{ project_id, file_id }` |\n| `amba_media_create_folder` | Create a logical folder. | `{ project_id, path: \"/uploads/avatars\" }` |\n| `amba_media_list_folders` | List folders. | `{ project_id }` |\n| `amba_media_delete_folder` | Delete a folder (must be empty). | `{ project_id, path }` |\n\n### Sites (static asset hosting)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_sites_deploy` | Deploy a static site bundle (zip / tar). | `{ project_id, name: \"marketing\", bundle: \"<base64>\", index: \"index.html\" }` |\n| `amba_sites_list` | List sites. | `{ project_id }` |\n| `amba_sites_get` | Read a site. | `{ project_id, site_id }` |\n| `amba_sites_add_domain` | Attach a custom domain. | `{ project_id, site_id, domain: \"marketing.example.com\" }` |\n| `amba_sites_list_domains` | List domains on a site. | `{ project_id, site_id }` |\n| `amba_sites_remove_domain` | Detach a domain. | `{ project_id, site_id, domain }` |\n| `amba_sites_delete` | Delete a site. | `{ project_id, site_id }` |\n\n### Purchased domains + email forwarding\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_domains_search` | Search available domains (free). | `{ project_id, query: \"myapp\" }` |\n| `amba_domains_check` | Check authoritative price + availability. | `{ project_id, domains: [\"myapp.com\"] }` |\n| `amba_domains_purchase` | Quote, then confirm, a domain purchase. | `{ project_id, domain: \"myapp.com\", site: \"marketing\" }` |\n| `amba_domains_list` | List purchased domains. | `{ project_id }` |\n| `amba_domains_email_enable` | Enable inbound routing when no MX conflict exists. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_destinations_add` | Add a destination mailbox; returns action-required until verified. | `{ project_id, domain: \"myapp.com\", email: \"owner@example.net\" }` |\n| `amba_domains_email_destinations_get` | Poll destination verification. | `{ project_id, domain: \"myapp.com\", destination_id }` |\n| `amba_domains_email_forwards_set` | Create/update a literal forward. | `{ project_id, domain: \"myapp.com\", source: \"support\", destination: \"owner@example.net\" }` |\n| `amba_domains_email_forwards_list` | List literal forwards. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_forwards_delete` | Delete a literal forward. | `{ project_id, domain: \"myapp.com\", forward_id }` |\n| `amba_domains_email_catch_all_set` | Enable/update/disable catch-all. | `{ project_id, domain: \"myapp.com\", enabled: true, destination: \"owner@example.net\" }` |\n| `amba_domains_email_catch_all_get` | Read catch-all state. | `{ project_id, domain: \"myapp.com\" }` |\n\n## SDK init per stack\n\n`Amba.configure(...)` runs first. The infrastructure surfaces \u2014 collections, AI, config, flags, events \u2014 are SDK-side reads; the snippets below show what the client calls look like.\n\n### Expo / React Native\n\n```tsx\nimport { Amba } from '@layers/amba-expo';\n\n// Collections \u2014 typed table, user-scoped reads + writes\ntype Todo = { id: string; title: string; done: boolean; created_at: string };\n\nconst { data: todos } = await Amba.collections.find<Todo>('todos', {\n filter: Amba.collections.where.eq('done', false),\n order: [{ column: 'created_at', direction: 'desc' }],\n limit: 50,\n});\n\nconst newTodo = await Amba.collections.insert('todos', { title: 'Ship the app', done: false });\nawait Amba.collections.update('todos', newTodo.id, { done: true });\nawait Amba.collections.delete('todos', newTodo.id);\n\n// AI \u2014 call a managed prompt (prompt_slug names the registered prompt)\nconst response = await Amba.ai.anthropic.messages.create({\n prompt_slug: 'summarize',\n variables: { text: 'A long article about backend services \u2026' },\n});\n\n// Track an analytics event\nawait Amba.events.track('button_clicked', { button: 'cta' });\n\n// Read runtime config\nconst config = await Amba.config.fetch();\n\n// Read a feature flag\nconst showBeta = await Amba.flags.get('beta_feature');\n\n// Diagnostics \u2014 wire-verify\nconst ping = await Amba.diagnostics.ping();\nif (!ping.ok) console.error('Amba misconfigured:', ping);\n```\n\n### Web\n\n```ts\nimport { Amba } from '@layers/amba-web';\n\nconst { data: todos } = await Amba.collections.find('todos', {\n filter: Amba.collections.where.eq('done', false),\n limit: 50,\n});\nawait Amba.collections.insert('todos', { title: 'Ship', done: false });\nawait Amba.events.track('page_view', { path: location.pathname });\n```\n\nWith `@layers/amba-react`:\n\n```tsx\nimport { useCollection, useFlag } from '@layers/amba-react';\n\nfunction TodoList() {\n const { data: todos, loading, refetch } = useCollection<{ id: string; title: string }>('todos');\n const showArchive = useFlag('archive_todos');\n if (loading) return <Spinner />;\n return (\n <ul>\n {todos?.map(t => <li key={t.id}>{t.title}</li>)}\n {showArchive && <ArchiveButton onArchive={refetch} />}\n </ul>\n );\n}\n```\n\n### iOS (Swift)\n\n```swift\nimport Amba\n\nstruct Todo: Codable {\n let id: String\n let title: String\n let done: Bool\n}\n\nlet response = try await Amba.collections.find(\"todos\", as: Todo.self)\n_ = try await Amba.collections.insert(\"todos\", row: [\"title\": \"Ship\", \"done\": false])\n\nlet config = try await Amba.config.fetch()\nlet showBeta = try await Amba.flags.get(name: \"beta_feature\")\ntry await Amba.events.track(\"app_opened\", properties: [\"source\": \"deep_link\"])\n\nlet reply = try await Amba.ai.anthropic.messages.create(\n request: AiMessageRequest(promptSlug: \"summarize\", variables: [\"text\": \"A long article...\"])\n)\n```\n\n### Android (Kotlin)\n\n```kotlin\ndata class Todo(val id: String, val title: String, val done: Boolean)\n\nval todos = Amba.collections.find<Todo>(\"todos\")\nAmba.collections.insert(\"todos\", mapOf(\"title\" to \"Ship\", \"done\" to false))\n\nval config = Amba.config.fetch()\nval showBeta = Amba.flags.get(\"beta_feature\")\nAmba.events.track(\"app_opened\", mapOf(\"source\" to \"deep_link\"))\n```\n\n### Flutter\n\n```dart\nimport 'package:amba/amba.dart';\n\nfinal response = await Amba.collections.find('todos', limit: 50);\nawait Amba.collections.insert('todos', {'title': 'Ship', 'done': false});\nfinal config = await Amba.config.fetch();\nfinal showBeta = await Amba.flags.get('beta_feature');\nawait Amba.events.track('app_opened', {'source': 'deep_link'});\n```\n\n## Common follow-ups\n\nBatch.\n\n1. **Custom data tables (collections):** any domain-specific tables to create?\n - Yes \u2014 I'll list them. (For each: name + columns + types.)\n - No, just use the canned Amba surfaces (auth, push, gamification, etc.)\n - Auto-create from the existing code's models \u2014 read `lib/models/`, `src/types/`, `Models/`, infer column lists, confirm with me.\n\n2. **Custom backend logic (functions):** any server-side code to deploy?\n - Yes \u2014 describe what it should do. (Then offer to scaffold a function template and deploy.)\n - No\n\n3. **AI features:** want managed LLM prompts?\n - Yes \u2014 what's the use case? (summarize, translate, classify, generate, custom)\n - No\n\n4. **Analytics:** which tracker do you want?\n - Only Amba's built-in events (recommended \u2014 already wired)\n - Amba + your own analytics pipeline (subscribe a webhook to project events via `amba_webhooks_create` and forward server-side)\n - None (rarely useful \u2014 events drive XP / achievements / streaks; disabling cripples gamification)\n\n5. **Third-party integrations to set up:**\n - [ ] RevenueCat (IAP / subscriptions on iOS + Android)\n - [ ] Superwall (paywall A/B)\n - [ ] Stripe Billing (web subscriptions through the app's own Stripe account \u2014 provider `stripe_billing`)\n - [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for `Amba.ai.*` \u2014 set via `amba_ai_providers_set`, **not** `amba_integrations_configure`)\n\n6. **Feature flags:** seed any starter flags?\n - Yes \u2014 wire `beta_feature` (off by default) so I can ship the wiring before the feature exists\n - No\n\n7. **Static site:** want a marketing page hosted under your tenant subdomain?\n - Yes \u2014 scaffold and deploy a 1-page index\n - No\n\n## Re-run behavior\n\n1. Before creating:\n - `amba_collections_list` \u2014 match on `name`. Collisions: never silently recreate (data loss). Offer `amba_collections_alter` to add new columns instead.\n - `amba_functions_list` \u2014 match on `name`. Collisions: ask to redeploy (with the new source) or skip.\n - `amba_ai_prompts_list` \u2014 match on `name`. Same. (And `amba_ai_providers_list` \u2014 match on `provider`; re-running `amba_ai_providers_set` rotates the key in place.)\n - `amba_integrations_list` \u2014 match on `provider`. Same.\n - `amba_configs_list` \u2014 match on `key`. Same.\n\n2. **Never call `amba_collections_delete` on re-run unless the user explicitly asks** \u2014 this drops the underlying table and every row in it across every user of the tenant.\n\n3. For functions: re-deploying replaces source in place (versioned server-side). It's safe to call `amba_functions_deploy` with the same name + new source.\n\n4. For integrations: if a provider is already configured, prefer `amba_integrations_patch` (partial update) over `amba_integrations_set` (full replace).\n\n5. Secrets: don't list secret values in chat output, even on read. Just confirm \"OPENAI_API_KEY is set\" / \"not set\".\n";
|
|
18
18
|
export declare const AMBA_SETUP_INFRASTRUCTURE_URI = "amba://setup/infrastructure";
|
|
19
19
|
export declare const AMBA_SETUP_INFRASTRUCTURE_MIME = "text/markdown";
|
|
@@ -93,7 +93,7 @@ export declare const AMBA_SETUP_SUB_RESOURCES: readonly [{
|
|
|
93
93
|
readonly name: "amba-setup-infrastructure";
|
|
94
94
|
readonly uri: "amba://setup/infrastructure";
|
|
95
95
|
readonly mime: "text/markdown";
|
|
96
|
-
readonly body: "# Infrastructure\n\nThe plumbing that sits behind every other surface: relational Postgres tables (Collections — schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Stripe / push credentials), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).\n\nIf gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.\n\n## MCP tools\n\n### Collections (relational Postgres tables)\n\nA collection is a relational Postgres table inside the project's isolated tenant database — typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.\n\nAdmin tools authenticate the developer/agent (pass `pat` or send it as the inbound Bearer) and take `project_id`. Client tools authenticate an end-user and take `api_key` (+ `session_token`) — NOT `project_id` and NOT a `pat`. Every row tool names the collection with `name`, never `collection`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_collections_create` | Create a typed collection. Pass `shared: true` for developer-seeded GLOBAL content (question banks, lookup tables) so `user_id` is nullable. | `{ project_id, name: \"todos\", columns: [{ name: \"title\", type: \"text\", nullable: false }, { name: \"done\", type: \"boolean\", nullable: false }, { name: \"due_at\", type: \"timestamptz\", nullable: true }], shared: false }` |\n| `amba_collections_list` | List collections in this project. | `{ project_id }` |\n| `amba_collections_get` | Read one collection's schema. | `{ project_id, name: \"todos\" }` |\n| `amba_collections_alter` | Exactly ONE of: `add_column`, `add_index`, `drop_column`, or `relax_user_id` per call. `relax_user_id: true` converts an existing collection to shared (drops the `user_id` NOT NULL). | `{ project_id, name: \"todos\", add_column: { name: \"priority\", type: \"integer\", nullable: true } }` |\n| `amba_collections_delete` | Drop the table (destructive). `confirm` must equal the collection name. | `{ project_id, name: \"todos\", confirm: \"todos\" }` |\n| `amba_admin_insert_row` | Insert one row as the developer (bypasses user-scope; `user_id` honored if present). | `{ project_id, name: \"todos\", row: { title: \"Sample\", done: false } }` |\n| `amba_admin_insert_rows` | Bulk-insert up to 500 rows in one atomic statement — the canonical seeding/migration path. `on_conflict`: `\"error\"` (default) or `\"skip\"`. | `{ project_id, name: \"questions\", rows: [{ q: \"...\" }, { q: \"...\" }], on_conflict: \"skip\" }` |\n| `amba_admin_list_rows` | Read rows as the developer. | `{ project_id, name: \"todos\", limit: 100 }` |\n| `amba_client_insert_row` | Insert as an end-user. Requires `api_key` (+ `session_token`). | `{ api_key, session_token, name: \"todos\", row: {...} }` |\n| `amba_client_list_rows` | Read as an end-user (auto user-scoped). | `{ api_key, session_token, name: \"todos\" }` |\n| `amba_client_get_row` | Get one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_update_row` | Update one row by id (end-user). Fields go in `set`. Omit `id` + pass `where` for a bulk update. | `{ api_key, session_token, name: \"todos\", id, set: {...} }` |\n| `amba_client_delete_row` | Soft-delete one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_count_rows` | Count rows matching an optional `where`. | `{ api_key, session_token, name: \"todos\", where: {...} }` |\n| `amba_client_find_rows` | Filter / sort / paginate rows (SDK-shaped `filter`). | `{ api_key, session_token, name: \"todos\", filter: {...}, order: [\"created_at desc\"], limit: 50 }` |\n| `amba_client_find_nearest_rows` | Vector-similarity search (rows with a `vector(<dim>)` column). | `{ api_key, session_token, name: \"todos\", column: \"embedding\", to_vector: [...], k: 10 }` |\n\nColumn types: `text`, `integer`, `bigint`, `numeric`, `boolean`, `timestamptz`, `date`, `jsonb`, `uuid`, `vector` (pass a separate `dimension` field, e.g. `{ name: \"embedding\", type: \"vector\", dimension: 1536 }` for OpenAI embeddings), plus array forms `text[]`, `integer[]`, `bigint[]`, `numeric[]`, `boolean[]`, `uuid[]`. Columns are NOT NULL unless `nullable: true`; column defaults are not supported (set values at insert time). Use `integer` (not `int`), `numeric` (not `float`/`real`/`double`), and `jsonb` (not `json`) — the validator rejects the aliases.\n\n### Functions (serverless code)\n\nRun user code in a sandbox triggered by HTTP, cron, or webhook. The function gets the tenant connection automatically via injected env.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_functions_deploy` | Deploy a function from source. | `{ project_id, name: \"send_welcome_email\", runtime: \"node22\", source: \"export default async (req) => { ... }\", trigger: { type: \"http\" } }` |\n| `amba_functions_list` | List functions. | `{ project_id }` |\n| `amba_functions_get` | Read function metadata. | `{ project_id, function_id }` |\n| `amba_functions_get_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |\n| `amba_functions_delete` | Delete a function. | `{ project_id, function_id }` |\n| `amba_functions_schedule` | Attach a cron schedule. | `{ project_id, function_id, cron: \"0 9 * * *\", timezone: \"America/Los_Angeles\" }` |\n| `amba_functions_pause_schedule` | Pause a scheduled trigger without deleting it. | `{ project_id, function_id }` |\n| `amba_functions_resume_schedule` | Resume. | `{ project_id, function_id }` |\n| `amba_functions_trigger_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |\n| `amba_function_domains_attach` | Attach one exact hostname to one function; returns DNS validation instructions. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_list` | List provider-neutral hostname, ownership, and certificate status. | `{ project_id, name: \"feed\" }` |\n| `amba_function_domains_refresh` | Re-poll DNS ownership and certificate state. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n\nFunction-domain routing preserves the complete incoming path and query string.\nIt is exact-host only (no wildcard/path rewrite and no automatic `www` for a\nsubdomain). Podcast/feed clients cannot attach an Amba API key, so their\nfunction must be deployed with `public: true` and validate any private token\ninside the handler. Effective-tier caps are free 1, pro 5, scale 20, and\nenterprise/comped 50; attach is limited to five attempts per project per hour.\nUnverified claims become eligible for reclaim after 24 hours, and routing\nresources are allocated only after ownership is active.\n\n### AI prompts\n\nManaged LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.\n\n**Two steps, in order:** first register the provider key with `amba_ai_providers_set`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with `provider_not_configured` (424) until the key is set.\n\n> The provider key is **not** a function secret. `amba_secrets_set` writes function-scoped Worker secrets, which the AI gateway never reads. Provider keys live in a separate gateway-owned store and are set **only** via `amba_ai_providers_set`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_ai_providers_set` | Register / rotate the upstream provider API key. **Do this first.** | `{ project_id, provider: \"anthropic\", api_key: \"sk-ant-...\" }` |\n| `amba_ai_providers_list` | List registered providers (`configured` = key set). | `{ project_id }` |\n| `amba_ai_providers_delete` | Revoke a provider key (fails if prompts still reference it). | `{ project_id, provider: \"anthropic\" }` |\n| `amba_ai_prompts_create` | Create a prompt template. `client_invokable: true` lets the device SDK invoke it directly. | `{ project_id, name: \"summarize\", provider: \"anthropic\", model: \"claude-opus-4-5\", system_prompt: \"Summarize the user's text in 2 sentences.\", client_invokable: true }` |\n| `amba_ai_prompts_list` | List prompts. | `{ project_id }` |\n| `amba_ai_prompts_get` | Read one prompt. | `{ project_id, name }` |\n| `amba_ai_prompts_update` | Edit a prompt (replaces all fields; bumps version). | `{ project_id, name, provider, model, system_prompt: \"...\" }` |\n| `amba_ai_prompts_invoke` | Invoke by name server-side (admin testing; works with `client_invokable: false`). Uses the named gateway path, so the prompt budget, rate limit, token cap, and spend attribution are enforced. | `{ project_id, name, messages: [{ role: \"user\", content: \"...\" }] }` |\n| `amba_ai_prompts_delete` | Delete. | `{ project_id, name }` |\n\n### Analytics + events + sessions\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_analytics_get` | Top-level metrics dashboard (MAU, DAU, retention). | `{ project_id, period: \"7d\" }` |\n| `amba_events_list` | Browse raw events. | `{ project_id, limit: 100, since: \"2026-05-19T00:00:00Z\" }` |\n| `amba_events_count` | Count events matching a filter. | `{ project_id, event: \"workout_completed\", since: \"...\" }` |\n| `amba_sessions_list` | List user sessions. | `{ project_id, limit: 50 }` |\n| `amba_sessions_analytics` | Session-level metrics. | `{ project_id, period: \"7d\" }` |\n| `amba_users_list_events` | Per-user event history. | `{ project_id, user_id }` |\n| `amba_users_export` | Export the full user list. | `{ project_id, format: \"csv\" }` |\n\n### Secrets + configs + integrations\n\nSecrets here become environment bindings on deployed functions. Omit `function`\nfor a project-wide secret or pass it to scope the value to one function. Setting\nor rotating a secret queues an asynchronous update for already-deployed\nfunctions; later deployments reconcile the binding too. They are NOT where AI\nprovider keys go (use `amba_ai_providers_set` for those — see AI prompts above).\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_secrets_set` | Set or rotate a function secret; omit `function` for project-wide scope or pass it for one function. Already-deployed functions receive it asynchronously. | `{ project_id, name: \"STRIPE_WEBHOOK_SECRET\", value: \"whsec_...\" }` |\n| `amba_secrets_get` | Read a secret (returns `\"<redacted>\"` unless explicitly requested). | `{ project_id, name }` |\n| `amba_secrets_list` | List secret names. | `{ project_id }` |\n| `amba_secrets_delete` | Delete. | `{ project_id, name }` |\n| `amba_configs_create` | Create a runtime config value (read from SDK as `Amba.config.fetch()`). | `{ project_id, key: \"primary_color\", value: \"#ff0066\", segment_id: null }` |\n| `amba_configs_list` | List configs. | `{ project_id }` |\n| `amba_configs_update` | Edit. | `{ project_id, config_id, value: \"...\" }` |\n| `amba_configs_delete` | Delete. | `{ project_id, config_id }` |\n| `amba_integrations_list` | List third-party integrations. | `{ project_id }` |\n| `amba_integrations_configure` | Configure a provider. | `{ project_id, provider: \"revenuecat\", config: { webhook_secret: \"...\", default_offering: \"...\" } }` |\n| `amba_integrations_set` | Set/replace integration config wholesale. | `{ project_id, provider, config }` |\n| `amba_integrations_patch` | Patch one field. | `{ project_id, provider, patch: { webhook_secret: \"...\" } }` |\n| `amba_integrations_test` | Send a test event to a configured provider. | `{ project_id, provider }` |\n\n### Media (file storage + CDN)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_media_upload` | Upload a file (returns a tenant-scoped URL). | `{ project_id, name: \"logo.png\", content_type: \"image/png\", data: \"<base64>\" }` |\n| `amba_media_list` | List files. | `{ project_id, folder: \"/\", limit: 100 }` |\n| `amba_media_delete` | Delete a file. | `{ project_id, file_id }` |\n| `amba_media_create_folder` | Create a logical folder. | `{ project_id, path: \"/uploads/avatars\" }` |\n| `amba_media_list_folders` | List folders. | `{ project_id }` |\n| `amba_media_delete_folder` | Delete a folder (must be empty). | `{ project_id, path }` |\n\n### Sites (static asset hosting)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_sites_deploy` | Deploy a static site bundle (zip / tar). | `{ project_id, name: \"marketing\", bundle: \"<base64>\", index: \"index.html\" }` |\n| `amba_sites_list` | List sites. | `{ project_id }` |\n| `amba_sites_get` | Read a site. | `{ project_id, site_id }` |\n| `amba_sites_add_domain` | Attach a custom domain. | `{ project_id, site_id, domain: \"marketing.example.com\" }` |\n| `amba_sites_list_domains` | List domains on a site. | `{ project_id, site_id }` |\n| `amba_sites_remove_domain` | Detach a domain. | `{ project_id, site_id, domain }` |\n| `amba_sites_delete` | Delete a site. | `{ project_id, site_id }` |\n\n### Purchased domains + email forwarding\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_domains_search` | Search available domains (free). | `{ project_id, query: \"myapp\" }` |\n| `amba_domains_check` | Check authoritative price + availability. | `{ project_id, domains: [\"myapp.com\"] }` |\n| `amba_domains_purchase` | Quote, then confirm, a domain purchase. | `{ project_id, domain: \"myapp.com\", site: \"marketing\" }` |\n| `amba_domains_list` | List purchased domains. | `{ project_id }` |\n| `amba_domains_email_enable` | Enable inbound routing when no MX conflict exists. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_destinations_add` | Add a destination mailbox; returns action-required until verified. | `{ project_id, domain: \"myapp.com\", email: \"owner@example.net\" }` |\n| `amba_domains_email_destinations_get` | Poll destination verification. | `{ project_id, domain: \"myapp.com\", destination_id }` |\n| `amba_domains_email_forwards_set` | Create/update a literal forward. | `{ project_id, domain: \"myapp.com\", source: \"support\", destination: \"owner@example.net\" }` |\n| `amba_domains_email_forwards_list` | List literal forwards. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_forwards_delete` | Delete a literal forward. | `{ project_id, domain: \"myapp.com\", forward_id }` |\n| `amba_domains_email_catch_all_set` | Enable/update/disable catch-all. | `{ project_id, domain: \"myapp.com\", enabled: true, destination: \"owner@example.net\" }` |\n| `amba_domains_email_catch_all_get` | Read catch-all state. | `{ project_id, domain: \"myapp.com\" }` |\n\n## SDK init per stack\n\n`Amba.configure(...)` runs first. The infrastructure surfaces — collections, AI, config, flags, events — are SDK-side reads; the snippets below show what the client calls look like.\n\n### Expo / React Native\n\n```tsx\nimport { Amba } from '@layers/amba-expo';\n\n// Collections — typed table, user-scoped reads + writes\ntype Todo = { id: string; title: string; done: boolean; created_at: string };\n\nconst { data: todos } = await Amba.collections.find<Todo>('todos', {\n filter: Amba.collections.where.eq('done', false),\n order: [{ column: 'created_at', direction: 'desc' }],\n limit: 50,\n});\n\nconst newTodo = await Amba.collections.insert('todos', { title: 'Ship the app', done: false });\nawait Amba.collections.update('todos', newTodo.id, { done: true });\nawait Amba.collections.delete('todos', newTodo.id);\n\n// AI — call a managed prompt (prompt_slug names the registered prompt)\nconst response = await Amba.ai.anthropic.messages.create({\n prompt_slug: 'summarize',\n variables: { text: 'A long article about backend services …' },\n});\n\n// Track an analytics event\nawait Amba.events.track('button_clicked', { button: 'cta' });\n\n// Read runtime config\nconst config = await Amba.config.fetch();\n\n// Read a feature flag\nconst showBeta = await Amba.flags.get('beta_feature');\n\n// Diagnostics — wire-verify\nconst ping = await Amba.diagnostics.ping();\nif (!ping.ok) console.error('Amba misconfigured:', ping);\n```\n\n### Web\n\n```ts\nimport { Amba } from '@layers/amba-web';\n\nconst { data: todos } = await Amba.collections.find('todos', {\n filter: Amba.collections.where.eq('done', false),\n limit: 50,\n});\nawait Amba.collections.insert('todos', { title: 'Ship', done: false });\nawait Amba.events.track('page_view', { path: location.pathname });\n```\n\nWith `@layers/amba-react`:\n\n```tsx\nimport { useCollection, useFlag } from '@layers/amba-react';\n\nfunction TodoList() {\n const { data: todos, loading, refetch } = useCollection<{ id: string; title: string }>('todos');\n const showArchive = useFlag('archive_todos');\n if (loading) return <Spinner />;\n return (\n <ul>\n {todos?.map(t => <li key={t.id}>{t.title}</li>)}\n {showArchive && <ArchiveButton onArchive={refetch} />}\n </ul>\n );\n}\n```\n\n### iOS (Swift)\n\n```swift\nimport Amba\n\nstruct Todo: Codable {\n let id: String\n let title: String\n let done: Bool\n}\n\nlet response = try await Amba.collections.find(\"todos\", as: Todo.self)\n_ = try await Amba.collections.insert(\"todos\", row: [\"title\": \"Ship\", \"done\": false])\n\nlet config = try await Amba.config.fetch()\nlet showBeta = try await Amba.flags.get(name: \"beta_feature\")\ntry await Amba.events.track(\"app_opened\", properties: [\"source\": \"deep_link\"])\n\nlet reply = try await Amba.ai.anthropic.messages.create(\n request: AiMessageRequest(promptSlug: \"summarize\", variables: [\"text\": \"A long article...\"])\n)\n```\n\n### Android (Kotlin)\n\n```kotlin\ndata class Todo(val id: String, val title: String, val done: Boolean)\n\nval todos = Amba.collections.find<Todo>(\"todos\")\nAmba.collections.insert(\"todos\", mapOf(\"title\" to \"Ship\", \"done\" to false))\n\nval config = Amba.config.fetch()\nval showBeta = Amba.flags.get(\"beta_feature\")\nAmba.events.track(\"app_opened\", mapOf(\"source\" to \"deep_link\"))\n```\n\n### Flutter\n\n```dart\nimport 'package:amba/amba.dart';\n\nfinal response = await Amba.collections.find('todos', limit: 50);\nawait Amba.collections.insert('todos', {'title': 'Ship', 'done': false});\nfinal config = await Amba.config.fetch();\nfinal showBeta = await Amba.flags.get('beta_feature');\nawait Amba.events.track('app_opened', {'source': 'deep_link'});\n```\n\n## Common follow-ups\n\nBatch.\n\n1. **Custom data tables (collections):** any domain-specific tables to create?\n - Yes — I'll list them. (For each: name + columns + types.)\n - No, just use the canned Amba surfaces (auth, push, gamification, etc.)\n - Auto-create from the existing code's models — read `lib/models/`, `src/types/`, `Models/`, infer column lists, confirm with me.\n\n2. **Custom backend logic (functions):** any server-side code to deploy?\n - Yes — describe what it should do. (Then offer to scaffold a function template and deploy.)\n - No\n\n3. **AI features:** want managed LLM prompts?\n - Yes — what's the use case? (summarize, translate, classify, generate, custom)\n - No\n\n4. **Analytics:** which tracker do you want?\n - Only Amba's built-in events (recommended — already wired)\n - Amba + your own analytics pipeline (subscribe a webhook to project events via `amba_webhooks_create` and forward server-side)\n - None (rarely useful — events drive XP / achievements / streaks; disabling cripples gamification)\n\n5. **Third-party integrations to set up:**\n - [ ] RevenueCat (IAP / subscriptions on iOS + Android)\n - [ ] Superwall (paywall A/B)\n - [ ] Stripe Billing (web subscriptions through the app's own Stripe account — provider `stripe_billing`)\n - [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for `Amba.ai.*` — set via `amba_ai_providers_set`, **not** `amba_integrations_configure`)\n\n6. **Feature flags:** seed any starter flags?\n - Yes — wire `beta_feature` (off by default) so I can ship the wiring before the feature exists\n - No\n\n7. **Static site:** want a marketing page hosted under your tenant subdomain?\n - Yes — scaffold and deploy a 1-page index\n - No\n\n## Re-run behavior\n\n1. Before creating:\n - `amba_collections_list` — match on `name`. Collisions: never silently recreate (data loss). Offer `amba_collections_alter` to add new columns instead.\n - `amba_functions_list` — match on `name`. Collisions: ask to redeploy (with the new source) or skip.\n - `amba_ai_prompts_list` — match on `name`. Same. (And `amba_ai_providers_list` — match on `provider`; re-running `amba_ai_providers_set` rotates the key in place.)\n - `amba_integrations_list` — match on `provider`. Same.\n - `amba_configs_list` — match on `key`. Same.\n\n2. **Never call `amba_collections_delete` on re-run unless the user explicitly asks** — this drops the underlying table and every row in it across every user of the tenant.\n\n3. For functions: re-deploying replaces source in place (versioned server-side). It's safe to call `amba_functions_deploy` with the same name + new source.\n\n4. For integrations: if a provider is already configured, prefer `amba_integrations_patch` (partial update) over `amba_integrations_set` (full replace).\n\n5. Secrets: don't list secret values in chat output, even on read. Just confirm \"OPENAI_API_KEY is set\" / \"not set\".\n";
|
|
96
|
+
readonly body: "# Infrastructure\n\nThe plumbing that sits behind every other surface: relational Postgres tables (Collections — schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Stripe / push credentials), media (file storage + CDN), and sites (static asset hosting at `*.app.amba.host`).\n\nIf gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.\n\n## MCP tools\n\n### Collections (relational Postgres tables)\n\nA collection is a relational Postgres table inside the project's isolated tenant database — typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in `app_user` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.\n\nAdmin tools authenticate the developer/agent (pass `pat` or send it as the inbound Bearer) and take `project_id`. Client tools authenticate an end-user and take `api_key` (+ `session_token`) — NOT `project_id` and NOT a `pat`. Every row tool names the collection with `name`, never `collection`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_collections_create` | Create a typed collection. Pass `shared: true` for developer-seeded GLOBAL content (question banks, lookup tables) so `user_id` is nullable. | `{ project_id, name: \"todos\", columns: [{ name: \"title\", type: \"text\", nullable: false }, { name: \"done\", type: \"boolean\", nullable: false }, { name: \"due_at\", type: \"timestamptz\", nullable: true }], shared: false }` |\n| `amba_collections_list` | List collections in this project. | `{ project_id }` |\n| `amba_collections_get` | Read one collection's schema. | `{ project_id, name: \"todos\" }` |\n| `amba_collections_alter` | Exactly ONE of: `add_column`, `add_index`, `drop_column`, or `relax_user_id` per call. `relax_user_id: true` converts an existing collection to shared (drops the `user_id` NOT NULL). | `{ project_id, name: \"todos\", add_column: { name: \"priority\", type: \"integer\", nullable: true } }` |\n| `amba_collections_delete` | Drop the table (destructive). `confirm` must equal the collection name. | `{ project_id, name: \"todos\", confirm: \"todos\" }` |\n| `amba_admin_insert_row` | Insert one row as the developer (bypasses user-scope; `user_id` honored if present). | `{ project_id, name: \"todos\", row: { title: \"Sample\", done: false } }` |\n| `amba_admin_insert_rows` | Bulk-insert up to 500 rows in one atomic statement — the canonical seeding/migration path. `on_conflict`: `\"error\"` (default) or `\"skip\"`. | `{ project_id, name: \"questions\", rows: [{ q: \"...\" }, { q: \"...\" }], on_conflict: \"skip\" }` |\n| `amba_admin_list_rows` | Read rows as the developer. | `{ project_id, name: \"todos\", limit: 100 }` |\n| `amba_client_insert_row` | Insert as an end-user. Requires `api_key` (+ `session_token`). | `{ api_key, session_token, name: \"todos\", row: {...} }` |\n| `amba_client_list_rows` | Read as an end-user (auto user-scoped). | `{ api_key, session_token, name: \"todos\" }` |\n| `amba_client_get_row` | Get one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_update_row` | Update one row by id (end-user). Fields go in `set`. Omit `id` + pass `where` for a bulk update. | `{ api_key, session_token, name: \"todos\", id, set: {...} }` |\n| `amba_client_delete_row` | Soft-delete one row by id (end-user). | `{ api_key, session_token, name: \"todos\", id }` |\n| `amba_client_count_rows` | Count rows matching an optional `where`. | `{ api_key, session_token, name: \"todos\", where: {...} }` |\n| `amba_client_find_rows` | Filter / sort / paginate rows (SDK-shaped `filter`). | `{ api_key, session_token, name: \"todos\", filter: {...}, order: [\"created_at desc\"], limit: 50 }` |\n| `amba_client_find_nearest_rows` | Vector-similarity search (rows with a `vector(<dim>)` column). | `{ api_key, session_token, name: \"todos\", column: \"embedding\", to_vector: [...], k: 10 }` |\n\nColumn types: `text`, `integer`, `bigint`, `numeric`, `boolean`, `timestamptz`, `date`, `jsonb`, `uuid`, `vector` (pass a separate `dimension` field, e.g. `{ name: \"embedding\", type: \"vector\", dimension: 1536 }` for OpenAI embeddings), plus array forms `text[]`, `integer[]`, `bigint[]`, `numeric[]`, `boolean[]`, `uuid[]`. Columns are NOT NULL unless `nullable: true`; column defaults are not supported (set values at insert time). Use `integer` (not `int`), `numeric` (not `float`/`real`/`double`), and `jsonb` (not `json`) — the validator rejects the aliases.\n\n### Functions (serverless code)\n\nRun user code in a sandbox triggered by HTTP, cron, or webhook. The function gets the tenant connection automatically via injected env.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_functions_deploy` | Deploy a function from source. | `{ project_id, name: \"send_welcome_email\", runtime: \"node22\", source: \"export default async (req) => { ... }\", trigger: { type: \"http\" } }` |\n| `amba_functions_list` | List functions. | `{ project_id }` |\n| `amba_functions_get` | Read function metadata. | `{ project_id, function_id }` |\n| `amba_functions_get_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |\n| `amba_functions_delete` | Delete a function. | `{ project_id, function_id }` |\n| `amba_functions_schedule` | Attach a cron schedule. | `{ project_id, function_id, cron: \"0 9 * * *\", timezone: \"America/Los_Angeles\" }` |\n| `amba_functions_pause_schedule` | Pause a scheduled trigger without deleting it. | `{ project_id, function_id }` |\n| `amba_functions_resume_schedule` | Resume. | `{ project_id, function_id }` |\n| `amba_functions_trigger_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |\n| `amba_function_domains_attach` | Attach one exact hostname to one function; returns DNS validation instructions. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_list` | List provider-neutral hostname, ownership, and certificate status. | `{ project_id, name: \"feed\" }` |\n| `amba_function_domains_refresh` | Re-poll DNS ownership and certificate state. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n| `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: \"feed\", hostname: \"feeds.example.com\" }` |\n\nFunction-domain routing preserves the complete incoming path and query string.\nIt is exact-host only (no wildcard/path rewrite and no automatic `www` for a\nsubdomain). Podcast/feed clients cannot attach an Amba API key, so their\nfunction must be deployed with `public: true` and validate any private token\ninside the handler. Effective-tier caps are free 1, pro 5, scale 20, and\nenterprise/comped 50; attach is limited to five attempts per project per hour.\nUnverified claims become eligible for reclaim after 24 hours, and routing\nresources are allocated only after ownership is active. Every hostname response\nincludes an unconditional `dns_note`: publish a real provider-stored CNAME;\nALIAS, ANAME, and copied A/AAAA addresses are not\nsubstitutes. Treat a hostname as ready only when `live=true`, never from TLS or\n`cert_status=active` alone.\n\n### AI prompts\n\nManaged LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.\n\n**Two steps, in order:** first register the provider key with `amba_ai_providers_set`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with `provider_not_configured` (424) until the key is set.\n\n> The provider key is **not** a function secret. `amba_secrets_set` writes function-scoped Worker secrets, which the AI gateway never reads. Provider keys live in a separate gateway-owned store and are set **only** via `amba_ai_providers_set`.\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_ai_providers_set` | Register / rotate the upstream provider API key. **Do this first.** | `{ project_id, provider: \"anthropic\", api_key: \"sk-ant-...\" }` |\n| `amba_ai_providers_list` | List registered providers (`configured` = key set). | `{ project_id }` |\n| `amba_ai_providers_delete` | Revoke a provider key (fails if prompts still reference it). | `{ project_id, provider: \"anthropic\" }` |\n| `amba_ai_prompts_create` | Create a prompt template. `client_invokable: true` lets the device SDK invoke it directly. | `{ project_id, name: \"summarize\", provider: \"anthropic\", model: \"claude-opus-4-5\", system_prompt: \"Summarize the user's text in 2 sentences.\", client_invokable: true }` |\n| `amba_ai_prompts_list` | List prompts. | `{ project_id }` |\n| `amba_ai_prompts_get` | Read one prompt. | `{ project_id, name }` |\n| `amba_ai_prompts_update` | Edit a prompt (replaces all fields; bumps version). | `{ project_id, name, provider, model, system_prompt: \"...\" }` |\n| `amba_ai_prompts_invoke` | Invoke by name server-side (admin testing; works with `client_invokable: false`). Uses the named gateway path, so the prompt budget, rate limit, token cap, and spend attribution are enforced. | `{ project_id, name, messages: [{ role: \"user\", content: \"...\" }] }` |\n| `amba_ai_prompts_delete` | Delete. | `{ project_id, name }` |\n\n### Analytics + events + sessions\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_analytics_get` | Top-level metrics dashboard (MAU, DAU, retention). | `{ project_id, period: \"7d\" }` |\n| `amba_events_list` | Browse raw events. | `{ project_id, limit: 100, since: \"2026-05-19T00:00:00Z\" }` |\n| `amba_events_count` | Count events matching a filter. | `{ project_id, event: \"workout_completed\", since: \"...\" }` |\n| `amba_sessions_list` | List user sessions. | `{ project_id, limit: 50 }` |\n| `amba_sessions_analytics` | Session-level metrics. | `{ project_id, period: \"7d\" }` |\n| `amba_users_list_events` | Per-user event history. | `{ project_id, user_id }` |\n| `amba_users_export` | Export the full user list. | `{ project_id, format: \"csv\" }` |\n\n### Secrets + configs + integrations\n\nSecrets here become environment bindings on deployed functions. Omit `function`\nfor a project-wide secret or pass it to scope the value to one function. Setting\nor rotating a secret queues an asynchronous update for already-deployed\nfunctions; later deployments reconcile the binding too. They are NOT where AI\nprovider keys go (use `amba_ai_providers_set` for those — see AI prompts above).\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_secrets_set` | Set or rotate a function secret; omit `function` for project-wide scope or pass it for one function. Already-deployed functions receive it asynchronously. | `{ project_id, name: \"STRIPE_WEBHOOK_SECRET\", value: \"whsec_...\" }` |\n| `amba_secrets_get` | Read a secret (returns `\"<redacted>\"` unless explicitly requested). | `{ project_id, name }` |\n| `amba_secrets_list` | List secret names. | `{ project_id }` |\n| `amba_secrets_delete` | Delete. | `{ project_id, name }` |\n| `amba_configs_create` | Create a runtime config value (read from SDK as `Amba.config.fetch()`). | `{ project_id, key: \"primary_color\", value: \"#ff0066\", segment_id: null }` |\n| `amba_configs_list` | List configs. | `{ project_id }` |\n| `amba_configs_update` | Edit. | `{ project_id, config_id, value: \"...\" }` |\n| `amba_configs_delete` | Delete. | `{ project_id, config_id }` |\n| `amba_integrations_list` | List third-party integrations. | `{ project_id }` |\n| `amba_integrations_configure` | Configure a provider. | `{ project_id, provider: \"revenuecat\", config: { webhook_secret: \"...\", default_offering: \"...\" } }` |\n| `amba_integrations_set` | Set/replace integration config wholesale. | `{ project_id, provider, config }` |\n| `amba_integrations_patch` | Patch one field. | `{ project_id, provider, patch: { webhook_secret: \"...\" } }` |\n| `amba_integrations_test` | Send a test event to a configured provider. | `{ project_id, provider }` |\n\n### Media (file storage + CDN)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_media_upload` | Upload a file (returns a tenant-scoped URL). | `{ project_id, name: \"logo.png\", content_type: \"image/png\", data: \"<base64>\" }` |\n| `amba_media_list` | List files. | `{ project_id, folder: \"/\", limit: 100 }` |\n| `amba_media_delete` | Delete a file. | `{ project_id, file_id }` |\n| `amba_media_create_folder` | Create a logical folder. | `{ project_id, path: \"/uploads/avatars\" }` |\n| `amba_media_list_folders` | List folders. | `{ project_id }` |\n| `amba_media_delete_folder` | Delete a folder (must be empty). | `{ project_id, path }` |\n\n### Sites (static asset hosting)\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_sites_deploy` | Deploy a static site bundle (zip / tar). | `{ project_id, name: \"marketing\", bundle: \"<base64>\", index: \"index.html\" }` |\n| `amba_sites_list` | List sites. | `{ project_id }` |\n| `amba_sites_get` | Read a site. | `{ project_id, site_id }` |\n| `amba_sites_add_domain` | Attach a custom domain. | `{ project_id, site_id, domain: \"marketing.example.com\" }` |\n| `amba_sites_list_domains` | List domains on a site. | `{ project_id, site_id }` |\n| `amba_sites_remove_domain` | Detach a domain. | `{ project_id, site_id, domain }` |\n| `amba_sites_delete` | Delete a site. | `{ project_id, site_id }` |\n\n### Purchased domains + email forwarding\n\n| Tool | Purpose | Example args |\n| --- | --- | --- |\n| `amba_domains_search` | Search available domains (free). | `{ project_id, query: \"myapp\" }` |\n| `amba_domains_check` | Check authoritative price + availability. | `{ project_id, domains: [\"myapp.com\"] }` |\n| `amba_domains_purchase` | Quote, then confirm, a domain purchase. | `{ project_id, domain: \"myapp.com\", site: \"marketing\" }` |\n| `amba_domains_list` | List purchased domains. | `{ project_id }` |\n| `amba_domains_email_enable` | Enable inbound routing when no MX conflict exists. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_destinations_add` | Add a destination mailbox; returns action-required until verified. | `{ project_id, domain: \"myapp.com\", email: \"owner@example.net\" }` |\n| `amba_domains_email_destinations_get` | Poll destination verification. | `{ project_id, domain: \"myapp.com\", destination_id }` |\n| `amba_domains_email_forwards_set` | Create/update a literal forward. | `{ project_id, domain: \"myapp.com\", source: \"support\", destination: \"owner@example.net\" }` |\n| `amba_domains_email_forwards_list` | List literal forwards. | `{ project_id, domain: \"myapp.com\" }` |\n| `amba_domains_email_forwards_delete` | Delete a literal forward. | `{ project_id, domain: \"myapp.com\", forward_id }` |\n| `amba_domains_email_catch_all_set` | Enable/update/disable catch-all. | `{ project_id, domain: \"myapp.com\", enabled: true, destination: \"owner@example.net\" }` |\n| `amba_domains_email_catch_all_get` | Read catch-all state. | `{ project_id, domain: \"myapp.com\" }` |\n\n## SDK init per stack\n\n`Amba.configure(...)` runs first. The infrastructure surfaces — collections, AI, config, flags, events — are SDK-side reads; the snippets below show what the client calls look like.\n\n### Expo / React Native\n\n```tsx\nimport { Amba } from '@layers/amba-expo';\n\n// Collections — typed table, user-scoped reads + writes\ntype Todo = { id: string; title: string; done: boolean; created_at: string };\n\nconst { data: todos } = await Amba.collections.find<Todo>('todos', {\n filter: Amba.collections.where.eq('done', false),\n order: [{ column: 'created_at', direction: 'desc' }],\n limit: 50,\n});\n\nconst newTodo = await Amba.collections.insert('todos', { title: 'Ship the app', done: false });\nawait Amba.collections.update('todos', newTodo.id, { done: true });\nawait Amba.collections.delete('todos', newTodo.id);\n\n// AI — call a managed prompt (prompt_slug names the registered prompt)\nconst response = await Amba.ai.anthropic.messages.create({\n prompt_slug: 'summarize',\n variables: { text: 'A long article about backend services …' },\n});\n\n// Track an analytics event\nawait Amba.events.track('button_clicked', { button: 'cta' });\n\n// Read runtime config\nconst config = await Amba.config.fetch();\n\n// Read a feature flag\nconst showBeta = await Amba.flags.get('beta_feature');\n\n// Diagnostics — wire-verify\nconst ping = await Amba.diagnostics.ping();\nif (!ping.ok) console.error('Amba misconfigured:', ping);\n```\n\n### Web\n\n```ts\nimport { Amba } from '@layers/amba-web';\n\nconst { data: todos } = await Amba.collections.find('todos', {\n filter: Amba.collections.where.eq('done', false),\n limit: 50,\n});\nawait Amba.collections.insert('todos', { title: 'Ship', done: false });\nawait Amba.events.track('page_view', { path: location.pathname });\n```\n\nWith `@layers/amba-react`:\n\n```tsx\nimport { useCollection, useFlag } from '@layers/amba-react';\n\nfunction TodoList() {\n const { data: todos, loading, refetch } = useCollection<{ id: string; title: string }>('todos');\n const showArchive = useFlag('archive_todos');\n if (loading) return <Spinner />;\n return (\n <ul>\n {todos?.map(t => <li key={t.id}>{t.title}</li>)}\n {showArchive && <ArchiveButton onArchive={refetch} />}\n </ul>\n );\n}\n```\n\n### iOS (Swift)\n\n```swift\nimport Amba\n\nstruct Todo: Codable {\n let id: String\n let title: String\n let done: Bool\n}\n\nlet response = try await Amba.collections.find(\"todos\", as: Todo.self)\n_ = try await Amba.collections.insert(\"todos\", row: [\"title\": \"Ship\", \"done\": false])\n\nlet config = try await Amba.config.fetch()\nlet showBeta = try await Amba.flags.get(name: \"beta_feature\")\ntry await Amba.events.track(\"app_opened\", properties: [\"source\": \"deep_link\"])\n\nlet reply = try await Amba.ai.anthropic.messages.create(\n request: AiMessageRequest(promptSlug: \"summarize\", variables: [\"text\": \"A long article...\"])\n)\n```\n\n### Android (Kotlin)\n\n```kotlin\ndata class Todo(val id: String, val title: String, val done: Boolean)\n\nval todos = Amba.collections.find<Todo>(\"todos\")\nAmba.collections.insert(\"todos\", mapOf(\"title\" to \"Ship\", \"done\" to false))\n\nval config = Amba.config.fetch()\nval showBeta = Amba.flags.get(\"beta_feature\")\nAmba.events.track(\"app_opened\", mapOf(\"source\" to \"deep_link\"))\n```\n\n### Flutter\n\n```dart\nimport 'package:amba/amba.dart';\n\nfinal response = await Amba.collections.find('todos', limit: 50);\nawait Amba.collections.insert('todos', {'title': 'Ship', 'done': false});\nfinal config = await Amba.config.fetch();\nfinal showBeta = await Amba.flags.get('beta_feature');\nawait Amba.events.track('app_opened', {'source': 'deep_link'});\n```\n\n## Common follow-ups\n\nBatch.\n\n1. **Custom data tables (collections):** any domain-specific tables to create?\n - Yes — I'll list them. (For each: name + columns + types.)\n - No, just use the canned Amba surfaces (auth, push, gamification, etc.)\n - Auto-create from the existing code's models — read `lib/models/`, `src/types/`, `Models/`, infer column lists, confirm with me.\n\n2. **Custom backend logic (functions):** any server-side code to deploy?\n - Yes — describe what it should do. (Then offer to scaffold a function template and deploy.)\n - No\n\n3. **AI features:** want managed LLM prompts?\n - Yes — what's the use case? (summarize, translate, classify, generate, custom)\n - No\n\n4. **Analytics:** which tracker do you want?\n - Only Amba's built-in events (recommended — already wired)\n - Amba + your own analytics pipeline (subscribe a webhook to project events via `amba_webhooks_create` and forward server-side)\n - None (rarely useful — events drive XP / achievements / streaks; disabling cripples gamification)\n\n5. **Third-party integrations to set up:**\n - [ ] RevenueCat (IAP / subscriptions on iOS + Android)\n - [ ] Superwall (paywall A/B)\n - [ ] Stripe Billing (web subscriptions through the app's own Stripe account — provider `stripe_billing`)\n - [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for `Amba.ai.*` — set via `amba_ai_providers_set`, **not** `amba_integrations_configure`)\n\n6. **Feature flags:** seed any starter flags?\n - Yes — wire `beta_feature` (off by default) so I can ship the wiring before the feature exists\n - No\n\n7. **Static site:** want a marketing page hosted under your tenant subdomain?\n - Yes — scaffold and deploy a 1-page index\n - No\n\n## Re-run behavior\n\n1. Before creating:\n - `amba_collections_list` — match on `name`. Collisions: never silently recreate (data loss). Offer `amba_collections_alter` to add new columns instead.\n - `amba_functions_list` — match on `name`. Collisions: ask to redeploy (with the new source) or skip.\n - `amba_ai_prompts_list` — match on `name`. Same. (And `amba_ai_providers_list` — match on `provider`; re-running `amba_ai_providers_set` rotates the key in place.)\n - `amba_integrations_list` — match on `provider`. Same.\n - `amba_configs_list` — match on `key`. Same.\n\n2. **Never call `amba_collections_delete` on re-run unless the user explicitly asks** — this drops the underlying table and every row in it across every user of the tenant.\n\n3. For functions: re-deploying replaces source in place (versioned server-side). It's safe to call `amba_functions_deploy` with the same name + new source.\n\n4. For integrations: if a provider is already configured, prefer `amba_integrations_patch` (partial update) over `amba_integrations_set` (full replace).\n\n5. Secrets: don't list secret values in chat output, even on read. Just confirm \"OPENAI_API_KEY is set\" / \"not set\".\n";
|
|
97
97
|
readonly surface: "infrastructure";
|
|
98
98
|
readonly title: "Amba setup — infrastructure";
|
|
99
99
|
readonly description: string;
|