@layers/amba-mcp 4.0.7 → 4.0.9
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/auto/collection-tools.d.ts +2 -0
- package/dist/auto/function-tools.d.ts +2 -0
- package/dist/index.js +47 -20
- package/dist/lib/tool-result.d.ts +12 -0
- package/dist/lib/with-pat.d.ts +6 -1
- package/dist/resources/amba-setup-infrastructure.d.ts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,8 @@ export interface CollectionPolicy {
|
|
|
51
51
|
export interface AppMcpClientAuth {
|
|
52
52
|
apiKey: string;
|
|
53
53
|
sessionToken?: string;
|
|
54
|
+
/** Verified by the client API on every request; never trusted locally. */
|
|
55
|
+
groupContext?: string;
|
|
54
56
|
baseUrl: string;
|
|
55
57
|
}
|
|
56
58
|
export interface RegisterCollectionToolsOptions {
|
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
* named `recipes`.
|
|
27
27
|
*/
|
|
28
28
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
29
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
29
30
|
import { type AppMcpClientAuth } from './collection-tools.js';
|
|
30
31
|
/** One exposed function from the app-MCP manifest. */
|
|
31
32
|
export interface AppMcpManifestFunction {
|
|
32
33
|
name: string;
|
|
33
34
|
description?: string | null;
|
|
34
35
|
input_schema?: Record<string, unknown> | null;
|
|
36
|
+
annotations?: ToolAnnotations | null;
|
|
35
37
|
public?: boolean;
|
|
36
38
|
invoke_url: string;
|
|
37
39
|
}
|
package/dist/index.js
CHANGED
|
@@ -641,9 +641,9 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
|
|
|
641
641
|
* `amba_developer_login`, `amba_developer_refresh`) — anything else
|
|
642
642
|
* should go through `registerTool`.
|
|
643
643
|
*/
|
|
644
|
-
function registerPublicTool(server, name, description, schema, handler, aliases = []) {
|
|
645
|
-
server.tool(name, description, schema, deriveToolAnnotations(name), handler);
|
|
646
|
-
for (const alias of aliases) server.tool(alias, description, schema, deriveToolAnnotations(alias), (async (args) => {
|
|
644
|
+
function registerPublicTool(server, name, description, schema, handler, aliases = [], annotations) {
|
|
645
|
+
server.tool(name, description, schema, annotations ?? deriveToolAnnotations(name), handler);
|
|
646
|
+
for (const alias of aliases) server.tool(alias, description, schema, annotations ?? deriveToolAnnotations(alias), (async (args) => {
|
|
647
647
|
warnDeprecatedAlias(alias, name);
|
|
648
648
|
return handler(args);
|
|
649
649
|
}));
|
|
@@ -1489,6 +1489,13 @@ function jsonResult$1(payload) {
|
|
|
1489
1489
|
text: JSON.stringify(payload, null, 2)
|
|
1490
1490
|
}] };
|
|
1491
1491
|
}
|
|
1492
|
+
/** Preserve an HTTP response wrapper while mapping failures to MCP isError. */
|
|
1493
|
+
function httpResult(result) {
|
|
1494
|
+
return {
|
|
1495
|
+
...jsonResult$1(result),
|
|
1496
|
+
...result.status >= 400 ? { isError: true } : {}
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1492
1499
|
/**
|
|
1493
1500
|
* Flatten an upstream HTTP response into the agent-facing tool payload.
|
|
1494
1501
|
* Body fields are spread FIRST so a future top-level `status` key in
|
|
@@ -1496,10 +1503,13 @@ function jsonResult$1(payload) {
|
|
|
1496
1503
|
* `parsed.status` to distinguish 2xx from 4xx/5xx.
|
|
1497
1504
|
*/
|
|
1498
1505
|
function passthroughResult(result) {
|
|
1499
|
-
return
|
|
1500
|
-
...
|
|
1501
|
-
|
|
1502
|
-
|
|
1506
|
+
return {
|
|
1507
|
+
...jsonResult$1({
|
|
1508
|
+
...result.body !== null && typeof result.body === "object" ? result.body : {},
|
|
1509
|
+
status: result.status
|
|
1510
|
+
}),
|
|
1511
|
+
...result.status >= 400 ? { isError: true } : {}
|
|
1512
|
+
};
|
|
1503
1513
|
}
|
|
1504
1514
|
//#endregion
|
|
1505
1515
|
//#region src/tools/users.ts
|
|
@@ -5755,14 +5765,17 @@ function registerTools$18(server, apiClient) {
|
|
|
5755
5765
|
"Function name must match /^[a-z][a-z0-9_-]*$/ and be ≤58 chars. Bundle size cap: 10 MiB (enforced client-side too — oversize uploads are rejected before the round-trip).",
|
|
5756
5766
|
"Optional `rate_limit` declares a per-function rate-limit config (validated server-side). Pass null/omit for no rate limit.",
|
|
5757
5767
|
"Optional `public` (default false) deploys the function WITHOUT the X-Api-Key gate, so third-party webhook senders (RevenueCat, Stripe, GitHub, Slack) that cannot attach a custom header can reach it. SECURITY: when public, YOU must verify the webhook signature (HMAC) inside the function body — the runtime no longer requires a caller credential. `public: true` only skips the key requirement; rate-limiting and the per-request signed identity context still apply. Leave it false for functions only your own app calls.",
|
|
5768
|
+
"Optional `forward_authorization` (default false) forwards a verified client Bearer to customer code. X-Api-Key and platform internal-trigger credentials are always stripped. Enable only when the function must validate or proxy the end-user session.",
|
|
5758
5769
|
"Returns the function deployment row + the public URL (`fn_url`: `https://{project_slug}.fn.amba.host/{name}`)."
|
|
5759
5770
|
].join(" "), {
|
|
5760
5771
|
project_id: z.string().describe("The Amba project ID."),
|
|
5761
5772
|
name: z.string().describe("Function name. Lowercase, /^[a-z][a-z0-9_-]*$/, ≤58 chars."),
|
|
5762
5773
|
code: z.string().describe("Bundled JavaScript module source (one entry file)."),
|
|
5763
5774
|
rate_limit: z.unknown().optional().describe("Optional rate-limit config object. Shape: see @layers/amba-shared:RateLimitConfig."),
|
|
5764
|
-
public: z.boolean().optional().describe("Deploy the function as PUBLIC (no X-Api-Key required) so external webhook senders can reach it. Default false. When true, you MUST verify the webhook HMAC signature inside the function body — public skips the credential gate only; rate-limiting and signed identity context still apply.")
|
|
5765
|
-
|
|
5775
|
+
public: z.boolean().optional().describe("Deploy the function as PUBLIC (no X-Api-Key required) so external webhook senders can reach it. Default false. When true, you MUST verify the webhook HMAC signature inside the function body — public skips the credential gate only; rate-limiting and signed identity context still apply."),
|
|
5776
|
+
forward_authorization: z.boolean().optional().describe("Forward a verified client Authorization Bearer to customer code. Default false. X-Api-Key and platform internal credentials are never forwarded."),
|
|
5777
|
+
mcp: z.unknown().optional().describe("Optional app-MCP tool config: {description?, input_schema?, annotations?}. Safety annotations support readOnlyHint, destructiveHint, idempotentHint, and openWorldHint booleans.")
|
|
5778
|
+
}, async ({ project_id, name, code, rate_limit, public: isPublic, forward_authorization: forwardAuthorization, mcp }, { pat }) => {
|
|
5766
5779
|
const byteLen = Buffer.byteLength(code, "utf8");
|
|
5767
5780
|
if (byteLen > MAX_BUNDLE_BYTES) throw new Error(`Bundle size ${byteLen} bytes exceeds ${MAX_BUNDLE_BYTES}-byte cap (10 MiB).`);
|
|
5768
5781
|
const form = new FormData();
|
|
@@ -5770,6 +5783,8 @@ function registerTools$18(server, apiClient) {
|
|
|
5770
5783
|
const metadata = { name };
|
|
5771
5784
|
if (rate_limit !== void 0 && rate_limit !== null) metadata.rate_limit = rate_limit;
|
|
5772
5785
|
if (isPublic === true) metadata.public = true;
|
|
5786
|
+
if (forwardAuthorization === true) metadata.forward_authorization = true;
|
|
5787
|
+
if (mcp !== void 0) metadata.mcp = mcp;
|
|
5773
5788
|
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
|
|
5774
5789
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/functions/deploy`, { formData: form }));
|
|
5775
5790
|
}, ["amba_deploy_function"]);
|
|
@@ -5826,6 +5841,7 @@ function registerTools$18(server, apiClient) {
|
|
|
5826
5841
|
"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
5842
|
"MVP constraints: exact lowercase hostnames only; no wildcards or path rewrites. Attaching a subdomain does NOT create a `www` hostname automatically.",
|
|
5828
5843
|
"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).",
|
|
5844
|
+
"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
5845
|
"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
5846
|
"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
5847
|
].join(" "), {
|
|
@@ -5858,6 +5874,9 @@ function registerTools$18(server, apiClient) {
|
|
|
5858
5874
|
deleted: true
|
|
5859
5875
|
});
|
|
5860
5876
|
});
|
|
5877
|
+
registerTool(server, apiClient, "amba_functions_list_schedules", "List function schedules with their cron expression, timezone, pause state, next fire time, and most recent fire time. Rows degrade with enriched=false if one provider describe call fails.", { project_id: z.string().describe("The Amba project ID.") }, async ({ project_id }, { pat }) => {
|
|
5878
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/functions/schedules`));
|
|
5879
|
+
});
|
|
5861
5880
|
registerTool(server, apiClient, "amba_functions_schedule", [
|
|
5862
5881
|
"Register a cron Schedule for a function. The schedule is keyed deterministically per (project, function) — re-calling with a different cron/timezone replaces the prior schedule rather than creating a duplicate.",
|
|
5863
5882
|
"Cron must be a standard 5- or 6-field whitespace-separated expression. `timezone` defaults to \"UTC\" if omitted. Timezones are IANA strings (e.g. \"America/Los_Angeles\").",
|
|
@@ -5972,7 +5991,8 @@ function registerTools$17(server, apiClient) {
|
|
|
5972
5991
|
registerTool(server, apiClient, "amba_sites_add_domain", [
|
|
5973
5992
|
"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
5993
|
"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
|
-
"
|
|
5994
|
+
"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.",
|
|
5995
|
+
"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
5996
|
].join(" "), {
|
|
5977
5997
|
project_id: z.string().describe("The Amba project ID."),
|
|
5978
5998
|
name: z.string().describe("Site name to attach the domain to."),
|
|
@@ -5981,7 +6001,7 @@ function registerTools$17(server, apiClient) {
|
|
|
5981
6001
|
const normalisedHostname = hostname.toLowerCase();
|
|
5982
6002
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/sites/${encodeURIComponent(name)}/domains`, { body: { hostname: normalisedHostname } }));
|
|
5983
6003
|
}, ["amba_add_site_domain"]);
|
|
5984
|
-
registerTool(server, apiClient, "amba_sites_list_domains", "List custom hostnames attached to a site,
|
|
6004
|
+
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
6005
|
project_id: z.string().describe("The Amba project ID."),
|
|
5986
6006
|
name: z.string().describe("Site name.")
|
|
5987
6007
|
}, async ({ project_id, name }, { pat }) => {
|
|
@@ -6036,10 +6056,10 @@ function registerTools$16(server, apiClient) {
|
|
|
6036
6056
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/check`, { body: { domains: normalised } }));
|
|
6037
6057
|
});
|
|
6038
6058
|
registerTool(server, apiClient, "amba_domains_purchase", [
|
|
6039
|
-
"Buy a domain through Amba and
|
|
6059
|
+
"Buy a domain through Amba and create a custom-domain binding to one of the project's sites.",
|
|
6040
6060
|
"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
6061
|
"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
|
-
"
|
|
6062
|
+
"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
6063
|
].join(" "), {
|
|
6044
6064
|
project_id: z.string().describe("The Amba project ID."),
|
|
6045
6065
|
domain: z.string().describe("The domain to buy, e.g. \"unbury.com\". Must be available (check first)."),
|
|
@@ -6061,7 +6081,7 @@ function registerTools$16(server, apiClient) {
|
|
|
6061
6081
|
if (years !== void 0) body.years = years;
|
|
6062
6082
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/purchase`, { body }));
|
|
6063
6083
|
});
|
|
6064
|
-
registerTool(server, apiClient, "amba_domains_list", "List domains this project has purchased through Amba, with registration status and
|
|
6084
|
+
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
6085
|
project_id: z.string().describe("The Amba project ID."),
|
|
6066
6086
|
limit: z.number().int().min(1).max(200).optional().describe("Page size (1-200, default 50)."),
|
|
6067
6087
|
offset: z.number().int().min(0).optional().describe("Skip rows (default 0).")
|
|
@@ -9322,7 +9342,11 @@ function must be deployed with \`public: true\` and validate any private token
|
|
|
9322
9342
|
inside the handler. Effective-tier caps are free 1, pro 5, scale 20, and
|
|
9323
9343
|
enterprise/comped 50; attach is limited to five attempts per project per hour.
|
|
9324
9344
|
Unverified claims become eligible for reclaim after 24 hours, and routing
|
|
9325
|
-
resources are allocated only after ownership is active.
|
|
9345
|
+
resources are allocated only after ownership is active. Every hostname response
|
|
9346
|
+
includes an unconditional \`dns_note\`: publish a real provider-stored CNAME;
|
|
9347
|
+
ALIAS, ANAME, and copied A/AAAA addresses are not
|
|
9348
|
+
substitutes. Treat a hostname as ready only when \`live=true\`, never from TLS or
|
|
9349
|
+
\`cert_status=active\` alone.
|
|
9326
9350
|
|
|
9327
9351
|
### AI prompts
|
|
9328
9352
|
|
|
@@ -10142,6 +10166,7 @@ const TOOL_CATEGORY = {
|
|
|
10142
10166
|
amba_functions_get_logs: "infrastructure",
|
|
10143
10167
|
amba_get_function_logs: "infrastructure",
|
|
10144
10168
|
amba_functions_schedule: "infrastructure",
|
|
10169
|
+
amba_functions_list_schedules: "infrastructure",
|
|
10145
10170
|
amba_schedule_function: "infrastructure",
|
|
10146
10171
|
amba_functions_pause_schedule: "infrastructure",
|
|
10147
10172
|
amba_pause_function_schedule: "infrastructure",
|
|
@@ -10700,6 +10725,7 @@ async function adminFetch(apiClient, bearer, call) {
|
|
|
10700
10725
|
async function clientFetch(clientAuth, call) {
|
|
10701
10726
|
const headers = { "X-Api-Key": clientAuth.apiKey };
|
|
10702
10727
|
if (clientAuth.sessionToken) headers["Authorization"] = `Bearer ${clientAuth.sessionToken}`;
|
|
10728
|
+
if (clientAuth.groupContext) headers["X-Group-Context"] = clientAuth.groupContext;
|
|
10703
10729
|
return proxyFetch(clientAuth.baseUrl, headers, call);
|
|
10704
10730
|
}
|
|
10705
10731
|
/**
|
|
@@ -11061,23 +11087,23 @@ function registerFunctionToolsFor(server, options) {
|
|
|
11061
11087
|
});
|
|
11062
11088
|
if (declaredShape) {
|
|
11063
11089
|
registerPublicTool(server, toolName, description, declaredShape, async (args) => {
|
|
11064
|
-
return
|
|
11090
|
+
return httpResult(await invokeFunction({
|
|
11065
11091
|
invokeUrl: fn.invoke_url,
|
|
11066
11092
|
clientAuth,
|
|
11067
11093
|
body: args
|
|
11068
11094
|
}));
|
|
11069
|
-
});
|
|
11095
|
+
}, [], fn.annotations ?? void 0);
|
|
11070
11096
|
return;
|
|
11071
11097
|
}
|
|
11072
11098
|
registerPublicTool(server, toolName, description, GENERIC_FUNCTION_SHAPE, async (args) => {
|
|
11073
11099
|
const { body, query } = args;
|
|
11074
|
-
return
|
|
11100
|
+
return httpResult(await invokeFunction({
|
|
11075
11101
|
invokeUrl: fn.invoke_url,
|
|
11076
11102
|
clientAuth,
|
|
11077
11103
|
body,
|
|
11078
11104
|
...query ? { query } : {}
|
|
11079
11105
|
}));
|
|
11080
|
-
});
|
|
11106
|
+
}, [], fn.annotations ?? void 0);
|
|
11081
11107
|
}
|
|
11082
11108
|
//#endregion
|
|
11083
11109
|
//#region src/auto/index.ts
|
|
@@ -11216,7 +11242,8 @@ async function registerAutoMcpTools(server, apiClient, options) {
|
|
|
11216
11242
|
fn,
|
|
11217
11243
|
clientAuth: {
|
|
11218
11244
|
apiKey: clientAuth.apiKey,
|
|
11219
|
-
...clientAuth.sessionToken ? { sessionToken: clientAuth.sessionToken } : {}
|
|
11245
|
+
...clientAuth.sessionToken ? { sessionToken: clientAuth.sessionToken } : {},
|
|
11246
|
+
...clientAuth.groupContext ? { groupContext: clientAuth.groupContext } : {}
|
|
11220
11247
|
}
|
|
11221
11248
|
});
|
|
11222
11249
|
return {
|
|
@@ -34,6 +34,17 @@ export declare function jsonResult(payload: unknown): {
|
|
|
34
34
|
text: string;
|
|
35
35
|
}[];
|
|
36
36
|
};
|
|
37
|
+
/** Preserve an HTTP response wrapper while mapping failures to MCP isError. */
|
|
38
|
+
export declare function httpResult(result: {
|
|
39
|
+
status: number;
|
|
40
|
+
body: unknown;
|
|
41
|
+
}): {
|
|
42
|
+
isError?: boolean | undefined;
|
|
43
|
+
content: {
|
|
44
|
+
type: "text";
|
|
45
|
+
text: string;
|
|
46
|
+
}[];
|
|
47
|
+
};
|
|
37
48
|
/**
|
|
38
49
|
* Flatten an upstream HTTP response into the agent-facing tool payload.
|
|
39
50
|
* Body fields are spread FIRST so a future top-level `status` key in
|
|
@@ -44,6 +55,7 @@ export declare function passthroughResult(result: {
|
|
|
44
55
|
status: number;
|
|
45
56
|
body: unknown;
|
|
46
57
|
}): {
|
|
58
|
+
isError?: boolean | undefined;
|
|
47
59
|
content: {
|
|
48
60
|
type: "text";
|
|
49
61
|
text: string;
|
package/dist/lib/with-pat.d.ts
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* the same call shape without the pat machinery.
|
|
42
42
|
*/
|
|
43
43
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
44
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
44
45
|
import { z, type ZodRawShape } from 'zod';
|
|
45
46
|
import type { ApiClient } from '../api-client.js';
|
|
46
47
|
/** Wire-shape returned by an MCP tool handler. */
|
|
@@ -49,6 +50,8 @@ export interface ToolResult {
|
|
|
49
50
|
type: 'text';
|
|
50
51
|
text: string;
|
|
51
52
|
}>;
|
|
53
|
+
/** MCP-native failure signal. Tool transports still return HTTP 200. */
|
|
54
|
+
isError?: boolean;
|
|
52
55
|
}
|
|
53
56
|
/** Context passed to handlers registered via `registerTool`. */
|
|
54
57
|
export interface WithPatHandlerContext {
|
|
@@ -122,4 +125,6 @@ export declare function registerPublicTool<S extends ZodRawShape>(server: McpSer
|
|
|
122
125
|
* warning per alias per process. No active auth-tool rename uses this
|
|
123
126
|
* yet, but the parameter exists so future renames stay consistent.
|
|
124
127
|
*/
|
|
125
|
-
aliases?: readonly string[]
|
|
128
|
+
aliases?: readonly string[],
|
|
129
|
+
/** Explicit app-declared safety hints; absent keeps conservative derivation. */
|
|
130
|
+
annotations?: ToolAnnotations): void;
|
|
@@ -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;
|