Docs / Connect your data
Base44
Count each call of your Base44 app's backend functions, ok or error, and get an alert when they spike, drop or stop.
In short: add a Webhook URL (from Senders (Connect)) as a Base44 secret and wrap each backend function with the helper. Each call counts one, ok or error.
The steps below work for you or your coding agent. Your agent does every step that needs no secret. The key comes from npx countwatch init (you approve it); a Webhook URL you make under Senders (Connect), or approve for your agent.
Counts each call of your app's backend functions as one hit to a Webhook URL, with the function's name and the outcome (ok or error) as tags. Each function × outcome is one watch (a push watch: it updates at once; mind the plan's watch limit and group related functions).
Base44 traps first: editing a function file (in the sandbox, or with Base44's edit_file, write_file or run_command) does not deploy it. Deploy each function ("Save & Redeploy" in the editor, or base44 functions deploy), then publish the app again: the published app keeps the old functions until then. Secrets reach functions as Deno.env, not the sandbox shell.
1. The Webhook URL
The person makes a Webhook URL for a metric named for what it counts (for example function_calls, not the app's name) in the dashboard (Senders (Connect), https://countwatch.com/dashboard/connections?tab=senders) and adds it as the Base44 secret COUNTWATCH_WEBHOOK_URL. It is a secret: keep it out of chat (link previews are ignored, but anyone who has the link can send counts).
2. The wrapper
Save as functions/shared/countwatch.ts (or next to each function):
// countwatch.ts — CountWatch for Base44 backend functions (Deno). Wrap a function's handler and each call
// sends one hit to COUNTWATCH_WEBHOOK_URL with ?function=<name>&outcome=ok|error: ok on 2xx, error on 5xx or a thrown
// error; a 4xx (the caller's mistake) is not counted. It never changes the response, waits at most 1.5 s, and does
// nothing when the secret is not set.
type Handler = (req: Request) => Response | Promise<Response>;
async function hit(fn: string, outcome: "ok" | "error"): Promise<void> {
const link = Deno.env.get("COUNTWATCH_WEBHOOK_URL");
if (!link) return;
const url = new URL(link);
url.searchParams.set("function", fn);
url.searchParams.set("outcome", outcome);
try {
await fetch(url, { method: "POST", signal: AbortSignal.timeout(1500) });
} catch {
// counting never breaks the function
}
}
export function withCountWatch(fn: string, handler: Handler): Handler {
return async (req) => {
let res: Response;
try {
res = await handler(req);
} catch (err) {
await hit(fn, "error");
throw err;
}
if (res.status >= 200 && res.status < 300) await hit(fn, "ok");
else if (res.status >= 500) await hit(fn, "error");
return res;
};
}
Wrap each function's handler:
import { withCountWatch } from "../shared/countwatch.ts";
Deno.serve(withCountWatch("finalize", async (req) => {
// … the function as before
return Response.json({ ok: true });
}));
Do not wrap the function that receives CountWatch alerts: its calls would feed back into the counts.
3. Deploy and test
Redeploy every wrapped function, then publish the app. A function that needs a signed-in user should answer 401 when there is none (base44.auth.me() throws; catch it): a 4xx is not counted, so signed-out visits and bots never add errors. Call a wrapped function that answers 200 without sign-in (or sign in and call one): its outcome:ok watch appears in check_setup.
4. Alerts to a function
A Base44 webhook trigger (https://cwebhooks.base44.com/api/hooks/…) queues the request and answers before your code runs, so it cannot answer the challenge. Use the receiving function's own URL, https://<app>.base44.app/functions/<name>, and answer the challenge in it:
// Standard Webhooks: HMAC-SHA256 of "id.timestamp.body" with the destination's signing secret, within 5 minutes;
// crypto.subtle.verify compares in constant time.
async function signed(req: Request, raw: string, secret: string | undefined): Promise<boolean> {
const id = req.headers.get("webhook-id"), ts = req.headers.get("webhook-timestamp") ?? "";
if (!secret || !id || !/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const bytes = (b64: string) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const key = await crypto.subtle.importKey("raw", bytes(secret.replace(/^whsec_/, "")), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
const data = new TextEncoder().encode(`${id}.${ts}.${raw}`);
for (const sig of (req.headers.get("webhook-signature") ?? "").split(" ")) {
if (sig.startsWith("v1,") && (await crypto.subtle.verify("HMAC", key, bytes(sig.slice(3)), data).catch(() => false))) return true;
}
return false;
}
Deno.serve(async (req) => {
const raw = await req.text(); // the signature covers these exact bytes
const body = JSON.parse(raw);
if (body.type === "destination.verify") return Response.json({ challenge: body.data.challenge });
if (!(await signed(req, raw, Deno.env.get("COUNTWATCH_WEBHOOK_SECRET")))) return new Response("bad signature", { status: 401 });
// … act on body (alert.opened, alert.resolved)
return new Response(null, { status: 204 });
});
Then manage {"action": "create_destination", "body": {"url": "https://<app>.base44.app/functions/<name>"}} and verify_destination. A signing secret is shown once: the person makes one in Destinations (Connect) → Edit → Get a new signing secret and puts it in the Base44 secret COUNTWATCH_WEBHOOK_SECRET; without it every alert is refused (401).
Check it works
- Data arrives.
check_setup(orcurl -s https://countwatch.com/v1/status -H "Authorization: Bearer $COUNTWATCH_API_KEY"): thefunction_callswatch showslast_request_at, andmissing_buckets_24hstays0(explicit watches). Do whatnext_actionssays. - History is in.
GET /v1/imports/{import_id}showsstatus: doneand the rules chosen forfunction_calls, with the alerts they would have raised. (Skipped the import? The watch learns from live data; starter rules apply meanwhile.) - An alert reaches you. The person turns on e-mail alerts in the dashboard (Destinations (Connect), https://countwatch.com/dashboard/connections?tab=destinations), or you add a webhook (
manage create_destination, thenverify_destination; the endpoint answers the challenge, see https://countwatch.com/agent). A write key cannot read alerts: read them with MCPalerts, or withGET /v1/alertsand an admin key. Thenmanage {"action": "send_test_alert"}: every active destination receives a test alert (test: true) within a few minutes. Real alerts go only where the watch's group chose (Alerts go to, ormanage update_groupwithdestination_ids); until then they stay in the app.
For agents: this page as Markdown.