GuidesAPI Auth
Vanity Pages Implementation
Embed hosted API key management pages with secure ticket issuance, tenant isolation, and operational guardrails.
Vanity Pages Implementation
This model embeds hosted API key management inside your product.
End-to-end flow
- User opens your API keys settings route.
- Backend validates RBAC + tenant ownership.
- Backend issues short-lived
api_auth_accessticket. - Frontend embeds hosted vanity page with ticket.
- User creates/rotates/revokes keys without leaving your product.
One-time app provisioning
import { WachtClient } from "@wacht/backend";
const client = new WachtClient({ apiKey: process.env.WACHT_BACKEND_API_KEY! });
await client.apiKeys.createApiAuthApp({
app_slug: "aa_42",
name: "Acme Public API",
key_prefix: "acme_live",
description: "API keys for deployment 42",
});Recommended naming convention: aa_<deploymentId>.
Backend ticket endpoint (Express)
import express from "express";
import { WachtClient } from "@wacht/backend";
const app = express();
app.use(express.json());
const wacht = new WachtClient({ apiKey: process.env.WACHT_BACKEND_API_KEY! });
app.post("/api/settings/api-keys/embed-ticket", async (req, res) => {
const user = req.user as { canManageApiKeys?: boolean; tenantId?: string } | undefined;
const deploymentId = req.body?.deploymentId as string | undefined;
if (!user) return res.status(401).json({ error: "Unauthorized" });
if (!user.canManageApiKeys) return res.status(403).json({ error: "Forbidden" });
if (!deploymentId) return res.status(400).json({ error: "deploymentId is required" });
// Add tenant ownership check here.
const ticket = await wacht.post<{ ticket: string; expires_at: number }>("/session/tickets", {
ticket_type: "api_auth_access",
api_auth_app_slug: `aa_${deploymentId}`,
expires_in: 120,
});
res.json(ticket);
});Frontend embed shell
import { useEffect, useMemo, useState } from "react";
import { useDeployment } from "@wacht/react-router";
export function ApiKeysPage({ deploymentId }: { deploymentId: string }) {
const { deployment } = useDeployment();
const [ticket, setTicket] = useState<string | null>(null);
useEffect(() => {
void fetch("/api/settings/api-keys/embed-ticket", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ deploymentId }),
})
.then((r) => r.json())
.then((data: { ticket: string }) => setTicket(data.ticket));
}, [deploymentId]);
const src = useMemo(() => {
if (!deployment?.backend_host || !ticket) return null;
return `${deployment.backend_host}/vanity/api-auth?ticket=${encodeURIComponent(ticket)}`;
}, [deployment?.backend_host, ticket]);
if (!src) return <div>Loading API key manager...</div>;
return <iframe src={src} title="API Key Manager" style={{ width: "100%", height: "80vh", border: 0 }} />;
}Operational guidance
- Reissue ticket on session expiry, not preemptively on every render.
- Log all ticket issuance attempts and denials.
- Keep TTL short and avoid storing raw tickets long-term.
- Verify embed routes in staging for all role permutations.
Security checklist
- Backend-only ticket issuance.
- RBAC enforced before issuing
api_auth_access. - Tenant isolation check on app slug mapping.
- No ticket issuance for soft-deleted/suspended tenants.
- Audit trail for key lifecycle actions is visible to support/security.