Skip to content

Backend-only tables

By default every database table is app-visible: any credential that can reach your app — including the browser-safe publishable key — can read and write it. That’s the right default for data the client owns.

Some data shouldn’t be directly reachable from the browser at all: audit logs, internal scoring, other users’ records, anything you want to gate behind your own authorization logic. Mark those tables backend-only.

Each table has an access class:

  • app (default) — reachable by any app credential, including a publishable key (pk). The normal client data plane.
  • backend — a publishable key is rejected outright (403). The table is reachable only with a non-publishable credential: a scoped access token (at) or a secret key (sk).

Because the browser only ever holds a pk, a backend-only table is structurally unreachable from client code. The only path to it is server-side — a serverless function you write, which applies its own authorization and acts as the gateway.

browser (pk) ──✗──▶ backend table (403: publishable key rejected)
browser (pk) ──▶ function (at) ──▶ backend table (function gates access)

In the developer portal, open your app → Database → the table → set its Visibility to Backend-only. Existing tables default to app-visible, so nothing changes until you opt a table in.

A function receives a scoped access token automatically as env.MUHKOO_ACCESS_TOKEN (mirrored to env.MUHKOO_APP_KEY), so it can reach backend tables with no manual key wiring:

import { Client } from "@muhkoo/connect";
export default {
async fetch(request, env) {
// 1) Apply YOUR authorization first.
const user = await authenticate(request);
if (!user) return new Response("Unauthorized", { status: 401 });
// 2) The injected token reaches backend-only tables the browser can't.
const client = new Client({
apiKey: env.MUHKOO_APP_KEY,
baseUrl: env.MUHKOO_API_URL,
});
const rows = await client
.db("audit_log")
.query({ where: [{ column: "user", op: "eq", value: user.id }] });
return Response.json(rows);
},
};

A browser calling the same table directly with the app’s publishable key gets a 403 — exactly the gate you want.

  • Sensitive or cross-user data — records a client must never read wholesale.
  • Server-authored data — audit trails, computed scores, moderation state.
  • Anything gated by custom rules — put the rules in a function and make the table backend-only so the function is the only way in.

For data the client legitimately owns and queries directly, leave the table app-visible — backend-only adds a round-trip through your function.