src/frontend/src/components/authorization/playgroundAuthGate/docs/shareable-playground-session-persistence.md
Generated on: 2026-04-06 Status: Review Owner: cristhianzl
Fixes authentication bypass on the shareable playground (/playground/:id/), persists chat sessions to the database for logged-in users, and adds token usage and build duration display to bot messages — aligning the shareable playground experience with the regular playground.
The shareable playground had three critical gaps:
AUTO_LOGIN=FALSE, the playground page bypassed authentication entirely. Any user with the link could access and execute public flows without logging in, causing 401/403 errors on API calls that required auth.window.sessionStorage. Sessions were lost on page refresh, couldn't be recovered across browser tabs, and provided no continuity for logged-in users.| Capability | AUTO_LOGIN=TRUE (anonymous) | AUTO_LOGIN=FALSE + not logged in | AUTO_LOGIN=FALSE + logged in |
|---|---|---|---|
| Access | Immediate (no login) | Redirect to /login | Immediate |
| Message storage | sessionStorage (browser only) | N/A (redirected) | Database (persistent) |
| Session persistence on refresh | Lost | N/A | Preserved |
| Session persistence across tabs | No (per-tab sessionStorage) | N/A | Yes (shared via DB) |
| User isolation | Per client_id cookie | N/A | Per user_id (deterministic UUID v5) |
| Token usage display | Yes (in-memory) | N/A | Yes (persisted) |
| Build duration display | Tokens only (duration not persisted) | N/A | Yes (tokens + duration, persisted) |
| Session rename/delete | sessionStorage operation | N/A | DB operation via /shared API |
| Virtual Flow ID seed | client_id (from cookie) | N/A | user_id (from auth) |
| API endpoints used | build_public_tmp only | N/A | build_public_tmp + /monitor/messages/shared/* |
| Build duration on refresh | Lost (not persisted to DB) | N/A | Preserved (persisted via /shared PUT) |
/monitor/messages/{id} PUT endpoint requires Flow ownership via JOIN, and the virtual flow_id doesn't exist in the Flow table. The /shared PUT endpoint is only used for authenticated users (AUTO_LOGIN=FALSE).sessionStorage), so opening the same playground in two tabs creates independent sessions.Playground Messaging — Encompasses authentication gating, message storage, retrieval, session management, and metadata display for both the regular flow editor playground and the shareable public playground.
| Context | Relationship | Integration Point |
|---|---|---|
| Authentication | Customer-Supplier | PlaygroundAuthGate consumes auth state; get_current_user_optional resolves user from cookies/tokens |
| Flow Execution | Partnership | build_public_tmp produces messages during flow builds; buildUtils.ts consumes build events |
| Monitor API | Conformist | New /shared endpoints conform to existing monitor API patterns and response types |
| Routing | Customer-Supplier | routes.tsx wraps playground route with PlaygroundAuthGate |
| Term | Definition | Code Reference |
|---|---|---|
| Shareable Playground | The public-facing playground at /playground/:id/ where users interact with shared flows | playgroundPage flag in useFlowStore |
| Regular Playground | The playground inside the flow editor, accessible only to the flow owner | PlaygroundModal component |
| Playground Auth Gate | A lightweight authentication wrapper that validates user session and controls access to the shareable playground | PlaygroundAuthGate component in playgroundAuthGate/index.tsx |
| Virtual Flow ID | A deterministic UUID v5 derived from an identifier (user_id or client_id) and the original flow_id, used for message isolation | compute_virtual_flow_id() in flow_utils.py |
| Source Flow ID | The original database flow ID from the URL (/playground/:id/), as opposed to the virtual flow ID | source_flow_id query parameter on /shared endpoints |
| Authenticated Playground | A shareable playground session where the user is logged in (not auto-login) and userData.id is available | isAuthenticatedPlayground() helper |
| Auto-Login | Server-side setting (AUTO_LOGIN) that allows anonymous access without explicit login. When enabled, users get a superuser session automatically | autoLogin in useAuthStore |
| Build Duration | The time in milliseconds from build start to ChatOutput completion for a single message segment | build_duration in message properties |
| Message Properties | A flexible JSON field on each message containing metadata: token usage, build duration, UI hints (icon, colors), feedback state | Properties Pydantic model, chat.properties in frontend |
| Session ID | A string identifier grouping messages within a flow. Defaults to the virtual flow ID; can be renamed by the user | session_id on MessageTable |
| Message Metadata | The inline badge showing token count and duration on bot messages, with a tooltip for detailed breakdown | MessageMetadata shared component |
| Session Validation | The process of checking if a user has a valid session via the /session endpoint using HttpOnly cookies | useGetAuthSession hook |
MessageTable (SQLModel ORM)id (UUID), flow_id (UUID, virtual for shared playground), session_id (string), text, sender, sender_name, properties (JSON), timestampProperties (contains build_duration, usage, state, icon, background_color, etc.), Usage (contains input_tokens, output_tokens, total_tokens)session_id, sender, and sender_nameflow_id is always a virtual UUID v5 (never the real flow ID)flow_id are only accessible to the user whose ID was used to derive that virtual IDuuid5(NAMESPACE_DNS, "{identifier}_{flow_id}")Flow table — it exists only as a flow_id on MessageTable rowsPlaygroundAuthGate componentLoading -> Authenticated | AutoLogin | RedirectToLoginuserData is set in both AuthContext and Zustand store before children render?redirect= query param| Event | Trigger | Payload | Consumers |
|---|---|---|---|
session_validated | useGetAuthSession returns | { authenticated, user } | PlaygroundAuthGate (sets auth state) |
auto_login_resolved | useGetAutoLogin returns | { autoLogin: boolean } | PlaygroundAuthGate (decides access) |
add_message | Flow component produces output | Message object with flow_id, session_id, text | message-event-handler.ts (React Query cache + Zustand store) |
end_vertex | ChatOutput vertex completes | VertexBuildData with build_data, id | buildUtils.ts (sets build_duration on last bot message) |
build_end | Entire flow build completes | flow_id, duration | buildUtils.ts (fallback build_duration if not set per-vertex) |
As a platform administrator I want the shareable playground to enforce authentication when AUTO_LOGIN=FALSE So that only authorized users can access and execute public flows
AUTO_LOGIN=FALSE/playground/:id//login?redirect=/playground/:id//login?redirect=/playground/:id//playground/:id//playground/:id/ in a new tabPlaygroundAuthGate validates the sessionAUTO_LOGIN=TRUE/playground/:id//playground/:id/As a logged-in user on the shareable playground I want my chat sessions to be saved to the database So that I can resume conversations after page refreshes and across browser sessions
/playground/:id//playground/:id/AUTO_LOGIN=TRUE/playground/:id/ without explicit loginwindow.sessionStorageAs a user on the shareable playground I want to see token usage and response time on bot messages So that I can monitor LLM consumption and performance
build_duration was recordedbuild_durationGET /monitor/messages/shared?order_by=__class__400 Bad Request with "Invalid order_by field"AUTO_LOGIN=TRUE and the user is on the shareable playgroundsessionStorage (no API call)autoLogin is still null (query not resolved) but isAuthenticated is trueisAuthenticatedPlayground() is evaluatedfalse (anonymous mode) to avoid UUID mismatchautoLogin resolves to false, the authenticated path activatesStatus: Accepted
The /playground/:id/ route existed outside the AppInitPage (which initializes auth) and ProtectedRoute (which enforces login). Adding it to ProtectedRoute would require modifying the global route structure, potentially breaking other routes.
Create a lightweight PlaygroundAuthGate component that wraps only the playground route. It independently validates the session via useGetAuthSession and checks auto-login via useGetAutoLogin, then decides: render children, redirect to login, or show loading.
Benefits:
userData in both AuthContext and Zustand store for downstream hooksTrade-offs:
Status: Accepted
Messages in the shareable playground need to be scoped per user. Adding a user_id column to MessageTable would require an Alembic migration.
Use the existing flow_id column with a deterministic virtual UUID v5 (uuid5(NAMESPACE_DNS, "{user_id}_{flow_id}")) as the implicit ownership marker. No new columns, no migrations.
Benefits:
MessageTableTrade-offs:
flow_id doesn't exist in Flow table — requires separate /shared endpoints/shared Endpoints Instead of Modifying Existing Monitor EndpointsStatus: Accepted
Existing monitor endpoints enforce ownership via JOIN Flow WHERE Flow.user_id = current_user.id. Virtual flow IDs don't exist in the Flow table.
Create parallel endpoints under /monitor/messages/shared/* that query by flow_id directly (no Flow JOIN). Existing endpoints remain unchanged.
Benefits:
Trade-offs:
MessageMetadata Component for Both PlaygroundsStatus: Accepted
Both playgrounds displayed token usage and build duration with identical tooltip content, icons, and formatting — ~130 lines duplicated.
Extract a shared MessageMetadata component and persistMessageProperties helper. Both playgrounds import the same component.
Benefits:
buildUtils.ts no longer knows about playground types or API routingTrade-offs:
autoLogin === falseStatus: Accepted
When autoLogin is null (unresolved), !null === true caused a brief window where the frontend computed a user-ID-based UUID while the backend used client-ID-based, resulting in UUID mismatch.
Use strict equality autoLogin === false instead of !autoLogin. Treat null (unresolved) as anonymous.
Benefits:
| Type | Name | Purpose |
|---|---|---|
| Database | MessageTable (message table) | Stores messages with virtual flow_id for shared playground |
| Auth Service | get_current_user_for_sse | Resolves user from cookies/API keys inside get_current_user_optional |
| Auth Hook | useGetAuthSession | Validates session via /session endpoint |
| Auth Hook | useGetAutoLogin | Checks auto-login setting via /auto_login endpoint |
| Store | useMessagesStore (Zustand) | Frontend in-memory message store; synced from API for authenticated playground |
| Store | useAuthStore (Zustand) | Holds isAuthenticated, autoLogin, userData for auth state |
| Library | uuid (Python + JavaScript) | UUID v5 generation for virtual flow IDs |
Purpose: List session IDs for a shared flow, scoped to the authenticated user.
Query Parameters:
source_flow_id: UUID (required) — The original public flow ID from the URL
Response (200):
["session-1", "Session Apr 06, 17:59:38"]
Purpose: Get messages for a shared flow, scoped to the authenticated user.
Query Parameters:
source_flow_id: UUID (required) — The original public flow ID
session_id: string (optional) — Filter by session
order_by: string (optional, default: "timestamp") — Allowed: timestamp, sender, sender_name, session_id, text
Response (200):
[
{
"id": "uuid",
"flow_id": "virtual-uuid",
"session_id": "Session Apr 06, 17:59:38",
"text": "Hello!",
"sender": "User",
"sender_name": "User",
"timestamp": "2026-04-06 17:59:38 UTC",
"properties": {
"build_duration": 1800,
"usage": { "total_tokens": 49, "input_tokens": 33, "output_tokens": 16 },
"state": "complete"
}
}
]
Purpose: Update a message on a shared flow (e.g., persist build_duration).
Query Parameters: source_flow_id: UUID (required)
Request Body: Partial MessageUpdate (e.g., { "properties": { "build_duration": 1800 } })
Response (200): Updated MessageRead object.
Response (404): Message not found or not owned by user.
Purpose: Delete all messages in a session on a shared flow.
Query Parameters: source_flow_id: UUID (required)
Response (204): No content (idempotent).
Purpose: Rename a session on a shared flow.
Query Parameters: new_session_id: string (required), source_flow_id: UUID (required)
Response (200): List of updated MessageResponse objects.
Response (404): No messages found.
| Error Code | Condition | User Message | Recovery Action |
|---|---|---|---|
| 400 | Invalid order_by field | "Invalid order_by field: {value}" | Use allowed values |
| 400 | No client_id AND no authenticated user | "No client_id cookie found" | Login or ensure cookies enabled |
| 403 | Missing/invalid authentication on shared endpoints | Standard 403 | Login and retry |
| 403 | Flow is not public | "Flow is not public" | Verify flow access_type |
| 404 | Message not found or not owned | "Message not found" | Verify source_flow_id |
| 404 | No messages in session (rename) | "No messages found with the given session ID" | Verify session exists |
| Metric | Type | Description | Alert Threshold |
|---|---|---|---|
shared_messages_fetched | Counter | Messages fetched via /shared endpoints | N/A (informational) |
shared_sessions_count | Gauge | Active sessions per user per flow | > 100 per user |
build_duration_persist_failures | Counter | Failed build_duration PUT requests | > 10 per minute |
playground_auth_redirects | Counter | Users redirected to login from playground | N/A (informational) |
| Log Level | Event | Fields | When |
|---|---|---|---|
| WARN | Failed to persist build_duration (shared) | messageId, error | PUT to shared endpoint fails |
| WARN | Failed to persist build_duration | messageId, error | PUT to standard endpoint fails |
| WARN | Public flow validation failed | Exception message | Invalid flow data in build_public_tmp |
| ERROR | Error building public flow | Exception details | Unexpected build error |
/shared endpoints via existing API metrics| Flag | Purpose | Default | Rollout Strategy |
|---|---|---|---|
| N/A | Controlled by AUTO_LOGIN setting and user authentication state | N/A | Full rollout with code deploy |
MessageTable columns with virtual flow IDs.Authentication:
AUTO_LOGIN=FALSE + not logged in: redirected to /login?redirect=/playground/:id/AUTO_LOGIN=FALSE + logged in: playground loads normallyAUTO_LOGIN=TRUE: playground works without login (no regression)Session Persistence:
Session Management:
Token Usage & Duration:
Regression:
graph TD
subgraph Users
AU["Authenticated User\n(logged in, AUTO_LOGIN=FALSE)"]
AN["Anonymous User\n(AUTO_LOGIN=TRUE)"]
OW["Flow Owner\n(creates and shares flows)"]
end
LF["Langflow\nAI flow builder with\nshareable playground"]
LLM["LLM Provider\n(OpenAI, Anthropic, etc.)"]
AU -->|"Sessions persisted to DB\n/shared endpoints"| LF
AN -->|"Sessions in sessionStorage\nbuild_public_tmp only"| LF
OW -->|"Creates flows, sets PUBLIC access"| LF
LF -->|"Executes LLM calls\nduring flow build"| LLM
graph TD
subgraph Frontend ["Frontend SPA (React + TypeScript)"]
GATE["PlaygroundAuthGate\nAuth check + redirect"]
PM["PlaygroundModal\nChat UI + session sidebar"]
HOOKS["Query Hooks\nuseGetMessagesQuery\nuseGetSessionsFromFlowQuery"]
META["MessageMetadata\nShared token/duration display"]
end
subgraph Backend ["Backend API (FastAPI + Python)"]
BUILD["build_public_tmp\n+ get_current_user_optional"]
SHARED["/monitor/messages/shared/*\n6 endpoints"]
AUTH["/session + /auto_login\nAuth validation"]
end
DB[("PostgreSQL\nMessageTable\n(virtual flow_id)")]
GATE -->|"validates session"| AUTH
PM --> HOOKS
PM --> META
HOOKS -->|"authenticated"| SHARED
HOOKS -->|"anonymous"| SS["sessionStorage\n(browser only)"]
PM -->|"send message"| BUILD
BUILD --> DB
SHARED --> DB
graph TD
USER[User visits /playground/:id/] --> PG[PlaygroundAuthGate]
PG -->|parallel| SC[useGetAuthSession]
PG -->|parallel| AL[useGetAutoLogin]
SC -->|authenticated: true| AUTH_OK[Set isAuthenticated + userData in stores]
SC -->|authenticated: false| CHECK_AL{autoLogin?}
AL -->|true| CHECK_AL
CHECK_AL -->|autoLogin=true| RENDER[Render Playground]
CHECK_AL -->|autoLogin=false, authenticated=false| REDIRECT[Redirect to /login]
AUTH_OK --> RENDER
RENDER --> PM[PlaygroundModal]
PM -->|uses| GFI[useGetFlowId]
GFI -->|isAuthenticated + userData.id| UUID_USER[uuid5 from userId]
GFI -->|anonymous| UUID_CLIENT[uuid5 from clientId]
graph TD
PM[PlaygroundModal] -->|queries| GMQ[useGetMessagesQuery]
PM -->|queries| GSQ[useGetSessionsFromFlowQuery]
GMQ -->|isAuthenticatedPlayground?| IAP{Check}
IAP -->|true| SAPI[/shared API endpoints/]
IAP -->|false| SS[sessionStorage]
GMQ -->|syncs to| MS[useMessagesStore]
MS -->|read by| CV[ChatView]
CV -->|renders| CM[ChatMessage]
CM -->|uses| MM[MessageMetadata component]
BU[buildUtils.ts] -->|sets build_duration| MS
BU -->|persists via| PMP[persistMessageProperties helper]
PMP -->|routes to| SAPI
PMP -->|routes to| RAPI[/standard API endpoints/]
graph TD
BPT[build_public_tmp] -->|optional auth| GCUO[get_current_user_optional]
GCUO -->|reads| COOKIE[access_token_lf cookie]
GCUO -->|reads| BEARER[Authorization header]
GCUO -->|reads| APIKEY[x-api-key]
BPT -->|calls| VPFU[verify_public_flow_and_get_user]
VPFU -->|uses| CVFI[compute_virtual_flow_id]
CVFI -->|user_id or client_id| VID[Virtual Flow ID]
BPT -->|starts| SFB[start_flow_build]
SFB -->|produces| MSG[Messages stored with virtual flow_id]
SE[/shared endpoints/] -->|require| GCAU[get_current_active_user]
SE -->|recompute| CVFI
SE -->|query| MT[MessageTable WHERE flow_id = virtual_id]