WebApps: admin-only access and DataPool viewers

Document information
  • Canonical URL: /docs/05_apps-webhooks-and-surfaces/webapps/50-Webapps_Admin_Only_Access
  • Version: 2026-09-29
  • Tags: webapps, authentication, datapool, tenant_admin, guides

Published WebApps do not automatically recognize console tenant_admin users. There is no platform setting that maps product login sessions to the WebApp runtime. If you need a published URL that lists rows from one DataPool schema and only tenant administrators may use it, you implement end-user WebApp auth plus role checks in ProcessFlow (and optional UI gating in page JavaScript).

This guide complements WebApps Authentication, which documents webapp_access tokens and auth_require. It does not replace DataPool or ProcessFlow references elsewhere in the library.

Two identities on every WebApp run

IdentityWhere it is configuredWhat it controls
webapp_user_idWebApp recordProcessFlow runs as this service account (tf.datapool, tf.api, integrations). Grant this user only the capabilities the app needs (for example read one schema).
End user (webapp_access)Login ProcessFlow + browser Bearer/cookieWho is allowed to call POST and /api/* when auth_require is on. Claims appear in process_input.auth after the runtime verifies the JWT.

The person viewing the page is not the same as webapp_user_id. A tenant admin signing in through your WebApp still triggers processes as webapp_user_id. Your step code must authorize the end user (for example process_input.auth.role === "tenant_admin") before returning DataPool rows.

What is not supported out of the box

  • auth_require alone does not restrict by platform role. Any valid webapp_access token for that WebApp passes the runtime gate.
  • Product POST /api/v1/auth/login from browser or step code as the WebApp session mechanism issues a console type: access token. That token is for /api/v1/*, not the published WebApp host, and must not be sent as the WebApp Bearer token. See WebApps Authentication.
  • webapp_access in the browser cannot call product /api/v1/* directly. List data inside ProcessFlow with tf.datapool (or proxy through your WebApp POST/api_router processes).

Optional pattern used by some tenant apps: the login ProcessFlow calls product login server-side (via tf.api as webapp_user_id) only to verify email and password, then issues jwt.issueWebappAccess for the WebApp. The browser never stores the console token. The AI adoption advisor describes that split for executive user accounts and rejects tenant_admin at the app — your admin-only app would invert that rule in step code instead of copying the advisor verbatim.

Recommended architecture: login WebApp + data WebApp

Use two published surfaces (or one WebApp with two processes and routing — login POST vs list POST is the common split).

Browser (tenant admin)
    │  POST login (auth_require off)
    ▼
Login ProcessFlow → verify admin → jwt.issueWebappAccess({ role: "tenant_admin", … })
    │  returns access_token (Bearer) or Set-Cookie
    ▼
Browser stores token (prefer in-memory for Bearer)
    │  POST list action with Authorization: Bearer
    ▼
List ProcessFlow (auth_require on) → check auth.role → tf.datapool.query → return rows

1. Service account (webapp_user_id)

Create or pick a webapp_user service account with the minimum ProcessFlow capabilities to read the target schema (typically datapool / api as required by your steps). Trace and operators set this on create_webapp via describe_webapp.webapp_users.

2. Login process

  • WebApp: auth_require: false, auth_transport: bearer (typical for custom JavaScript admin UI) or cookie for same-origin forms.
  • Validate credentials with your rules, for example:
    • Query Users (via tenantDb) for the email and confirm role (or permission) is tenant_admin (or your tenant’s admin role name), or
    • Call product login through tf.api.post("/api/v1/auth/login", …) inside the step, inspect the response, and discard the console token — only mint webapp_access with jwt.issueWebappAccess.
  • On success:
const issued = jwt.issueWebappAccess({
  user_id: adminUserId,
  email: adminEmail,
  role: "tenant_admin"
});

return {
  success: true,
  access_token: issued.access_token,
  token_type: "Bearer",
  expires_at: issued.expires_at,
  data: { authenticated: true }
};

Never log passwords or full JWTs. See password and token rules in WebApps Authentication.

3. List / read process

  • Same or sibling WebApp with auth_require: true.
  • At the start of the step:
const auth = process_input.auth;
if (!auth || auth.role !== "tenant_admin") {
  return {
    success: false,
    error: { message: "Forbidden", code: "FORBIDDEN" }
  };
}
  • Query one schema (example shape — adjust to your table name and filters):
const schemaId = "<YOUR_SCHEMA_ID>";
const rows = await tf.datapool.query(schemaId, {
  limit: 100,
  offset: 0
});

return { success: true, data: { rows } };

Process execution still runs as webapp_user_id. The admin check is application-level on process_input.auth, not impersonation of the admin inside the sandbox.

4. Browser client

POST responses use the runtime wrapper; business fields are under result.output_data. See WebApp–ProcessFlow integration — Outbound response envelopes.

function unwrapProcessPost(data) {
  return data && data.result && data.result.output_data !== undefined
    ? data.result.output_data
    : data;
}

async function loadRows(token) {
  const res = await fetch(window.location.href, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + token
    },
    body: JSON.stringify({ action: "list" })
  });
  const envelope = await res.json();
  const payload = unwrapProcessPost(envelope);
  if (!payload.success) {
    throw new Error(payload.error?.message ?? "Request failed");
  }
  return payload.data.rows;
}

Gate the page UI until a token exists; hide list controls when login failed.

DataPool scope and safety

  • Prefer one schema per app and read-only steps unless you explicitly need writes.
  • Return only columns the admin UI needs; avoid leaking internal IDs or PII you do not intend to expose on a public URL.
  • Use webapp_user_id permissions so the service account cannot read unrelated schemas even if step code is wrong — defense in depth with the tenant_admin check in code.

For DataPool concepts outside WebApps, see Data and Documents Introduction and platform skills documentation for operators using Trace (skill_id: datapool).

When to use the product console instead

If every viewer already works inside the Tealfabric tenant console (Next.js app) and you do not need a standalone public URL, building a console feature with normal product tenant_admin session auth is usually simpler than a custom WebApp login stack.

Choose a published WebApp when you need a dedicated hostname, a lightweight viewer, or an experience outside the main console chrome.

Checklist

  1. webapp_user_id service account with minimal DataPool read access.
  2. Login process issues webapp_access only after tenant_admin (or equivalent) verification.
  3. Data process enforces process_input.auth.role before tf.datapool.query.
  4. auth_require: true on protected WebApp; false on login surface.
  5. Client unwraps result.output_data and sends Authorization: Bearer for Bearer transport.
  6. Published version tested on the real URL (not preview-only GET).

Related guides