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
| Identity | Where it is configured | What it controls |
|---|---|---|
webapp_user_id | WebApp record | ProcessFlow 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/cookie | Who 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_requirealone does not restrict by platform role. Any validwebapp_accesstoken for that WebApp passes the runtime gate.- Product
POST /api/v1/auth/loginfrom browser or step code as the WebApp session mechanism issues a consoletype: accesstoken. 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_accessin the browser cannot call product/api/v1/*directly. List data inside ProcessFlow withtf.datapool(or proxy through your WebApp POST/api_routerprocesses).
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) orcookiefor same-origin forms. - Validate credentials with your rules, for example:
- Query
Users(viatenantDb) for the email and confirmrole(or permission) istenant_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 mintwebapp_accesswithjwt.issueWebappAccess.
- Query
- 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_idpermissions so the service account cannot read unrelated schemas even if step code is wrong — defense in depth with thetenant_admincheck 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
webapp_user_idservice account with minimal DataPool read access.- Login process issues
webapp_accessonly aftertenant_admin(or equivalent) verification. - Data process enforces
process_input.auth.rolebeforetf.datapool.query. auth_require: trueon protected WebApp;falseon login surface.- Client unwraps
result.output_dataand sendsAuthorization: Bearerfor Bearer transport. - Published version tested on the real URL (not preview-only GET).