Extension points are exported functions in src/lib/custom/ that you can override to add deployment-specific behavior. Each has a no-op default — Pika works correctly without any overrides.
All files under src/lib/custom/ are protected from pika sync, so your overrides survive framework updates.
Available Extension Points
Section titled “Available Extension Points”| File | Export | Default | Purpose |
|---|---|---|---|
site-admin.ts | isUserAllowedAdminAccess(user) | delegates to isUserSiteAdmin() | Gate admin routes on custom criteria |
additional-session-sources.ts | getAdditionalSessionSources(user, chatAppId) | [] | Add 0..N custom session sources to the sidebar |
session-read-only.ts | isSessionReadOnly(session, user) | false | Mark additional session types as read-only |
request-user-id-resolver.ts | resolveRequestUserId(requestedUserId, sessionUserId, ctx) | undefined | Override the user id used by message routes |
session-entity-extraction.ts | getSessionEntityValue(session) | session.entityId | Extract entity/account ID from a session |
session-account-context.ts | transformSessionAccountContext(session, user) | session unchanged | Backfill missing account context before sessions are returned |
server-hooks.ts | transformCustomUserData(data, ctx) | data unchanged | Transform customUserData before it reaches the converse Lambda |
server-hooks.ts | onAuthProviderCallback(event, provider) | no-op | Run logic on OAuth provider callbacks |
server-hooks.ts | onBeforeAuth(event, pathName, user) | { clearSession: false } | Clear the session conditionally before auth proceeds |
chat-user-auth.ts | shouldBypassChatUserRoleMerge(user) | false | Use token roles as source-of-truth; skip DDB role merge |
admin/site-admin-extensions.ts | customAdminPages | [] | Add pages to the Site Admin nav |
message-visibility.ts | shouldRenderMessage(message) | true | Hide messages from the rendered transcript without removing them from the session |
trace-content-access.ts | mayReceiveTraceContent(user, tracesFeatureEnabled) | true | Decide whether a user may receive trace content (system prompt, tool inputs and outputs) |
chat-html-allowed-tags.ts | allowedCustomTags, allowedCustomAttrs | [], {} | Allow custom elements, or lift a default ban such as img, in the chat and trace HTML sanitizer |
How to Override
Section titled “How to Override”- Open the relevant file in
src/lib/custom/. - Replace the default function body with your implementation.
- The framework calls your override automatically.
Example — require AzureAD for admin access:
import { isUserSiteAdmin } from '$lib/server/utils';import type { AuthenticatedUser, RecordOrUndef } from 'pika-shared/types/chatbot/chatbot-types';
export async function isUserAllowedAdminAccess( user: AuthenticatedUser<RecordOrUndef, RecordOrUndef>): Promise<boolean> { return isUserSiteAdmin(user) && user.authData?.provider === 'azuread';}Hook Reference
Section titled “Hook Reference”isUserAllowedAdminAccess — site-admin.ts
Section titled “isUserAllowedAdminAccess — site-admin.ts”Controls which users can access the site admin panel and session insights.
export async function isUserAllowedAdminAccess( user: AuthenticatedUser<RecordOrUndef, RecordOrUndef>): Promise<boolean>The default delegates to the built-in isUserSiteAdmin(user) check. Override to add additional criteria such as provider checks, group membership, or external permission lookups.
getAdditionalSessionSources — additional-session-sources.ts
Section titled “getAdditionalSessionSources — additional-session-sources.ts”Adds 0..N custom session sources to the sidebar. Each source has its own loader, optional sidebar slots, and optional per-source read-only predicate. Pika iterates the returned array in declared order and renders one Sidebar.Group per source.
export async function getAdditionalSessionSources( user: ChatUser<RecordOrUndef>, chatAppId: string): Promise<SessionSource[]>
export interface SessionSource { /** Stable identifier for this source. Used as a Svelte key and for diagnostics. */ id: string; /** Header label rendered above this source's session list. */ label?: string; /** * Loads sessions for this source. Called client-side once per chat-app init, * in parallel with other sources via Promise.allSettled. Capture user/chatAppId * from the enclosing getAdditionalSessionSources closure when constructing this * descriptor — the framework does not re-pass them. * * Return [] for "applicable but empty." If the source is not applicable to this * user, omit it from the getAdditionalSessionSources return array entirely. */ load(): Promise<ChatSession<RecordOrUndef>[]>; /** Per-source read-only predicate. OR-ed with the top-level isSessionReadOnly hook. */ isReadOnly?: (session: ChatSession<RecordOrUndef>) => boolean; /** * Optional sidebar slots: * - `trigger` renders before load completes (e.g. a "Load sessions" button) * - `header` renders inside Sidebar.GroupLabel above the loaded session list */ sidebarSlot?: { header?: Component<Record<string, never>>; trigger?: Component<Record<string, never>>; };}Rendering semantics: the sidebar renders each source's group through three states:
- Loading: if
sidebarSlot.triggeris set, render it; otherwise render a default loading row. - Loaded: render
sidebarSlot.header(if set) insideSidebar.GroupLabel, then the session list (or "No sessions found." when empty). - Errored: render
sidebarSlot.header(if set) and an inline error row. The group does not collapse; sibling sources are unaffected.
Sources load via Promise.allSettled, so one failing source never breaks the others. The default returns [] (no additional sources).
isSessionReadOnly — session-read-only.ts
Section titled “isSessionReadOnly — session-read-only.ts”Marks additional session types as read-only in the chat UI. Applies cross-cutting rules — for per-source rules, prefer SessionSource.isReadOnly.
export function isSessionReadOnly( session: ChatSession<RecordOrUndef> | undefined, user: ChatUser<RecordOrUndef> | undefined): booleanThe framework already marks shared sessions as read-only. Override this hook to add further conditions — for example, marking sessions belonging to a different effective user as read-only. The result is OR-ed with the framework's shared-session predicate and with each SessionSource.isReadOnly. The default returns false.
resolveRequestUserId — request-user-id-resolver.ts
Section titled “resolveRequestUserId — request-user-id-resolver.ts”Server-side hook called on message routes before the effective user id is used. Override to honor an alternate user id source (e.g. a separate cookie for legacy actions).
export async function resolveRequestUserId( requestedUserId: string, sessionUserId: string, context: RequestUserIdResolverContext): Promise<string | undefined>
export interface RequestUserIdResolverContext { request: Request; cookies: Cookies; stage: string; chatAppId?: string;}Return a non-undefined string to override the user id used for the request; return undefined (the default) to leave requestedUserId unchanged. Scope is limited to the message routes (api/message/+server.ts and api/message/[chatAppId]/[sessionId]/+server.ts).
Consumers that need a cookie name should define their own constant — the framework no longer exports one.
getSessionEntityValue — session-entity-extraction.ts
Section titled “getSessionEntityValue — session-entity-extraction.ts”Extracts the entity or account ID from a session for display and analytics.
export function getSessionEntityValue( session: ChatSession<RecordOrUndef>): string | undefinedOverride when your deployment stores the relevant account context in a field other than session.entityId, or when you need to normalize/transform the raw value. The default returns session.entityId || undefined.
transformSessionAccountContext — session-account-context.ts
Section titled “transformSessionAccountContext — session-account-context.ts”Backfills missing account context onto sessions before they are returned to the client.
export function transformSessionAccountContext( session: ChatSession<RecordOrUndef>, user: AuthenticatedUser<RecordOrUndef, RecordOrUndef>): ChatSession<RecordOrUndef>Called on every session returned from the sessions API and the site-admin search endpoint. Override to copy account data from user onto sessions that were created before account context was stored on sessions. Return a new object rather than mutating the input. The default returns the session unchanged.
onBeforeAuth — server-hooks.ts
Section titled “onBeforeAuth — server-hooks.ts”Called on every request after the AUTH_USER cookie is deserialized but before the ChatUser refresh and auth-provider validation steps run.
export async function onBeforeAuth( event: RequestEvent, pathName: string, user: AuthenticatedUser<RecordOrUndef, RecordOrUndef> | undefined): Promise<{ clearSession: boolean }>Return { clearSession: true } to atomically clear all cookies and drop the current session — the request continues as unauthenticated, triggering a fresh auth flow. Use this to discard stale sessions based on path, request headers, or cookie state without editing the synced hooks.server.ts. The default returns { clearSession: false }.
shouldBypassChatUserRoleMerge — chat-user-auth.ts
Section titled “shouldBypassChatUserRoleMerge — chat-user-auth.ts”Controls whether the framework uses roles from the authentication token instead of merging with the stored ChatUser record in the database.
export function shouldBypassChatUserRoleMerge( user: AuthenticatedUser<RecordOrUndef, RecordOrUndef>): booleanWhen this returns true the framework will:
- Treat roles as unchanged during periodic refreshes (no cookie re-serialization for role drift)
- Use token roles instead of database roles when building the merged user object
- Skip
mergeAuthenticatedUserWithExistingChatUseron initial login - Create the ChatUser record if it is missing during a refresh rather than forcing re-authentication
This is useful when the identity provider is the authoritative source for roles and the database should not override them. The default returns false.
transformCustomUserData — server-hooks.ts
Section titled “transformCustomUserData — server-hooks.ts”Transforms customUserData server-side before it is encoded into the message JWT and sent to the converse Lambda.
See Server Hooks for full documentation.
onAuthProviderCallback — server-hooks.ts
Section titled “onAuthProviderCallback — server-hooks.ts”Called from hooks.server.ts whenever a request arrives at an OAuth provider callback path (/auth/callback/<provider>).
export async function onAuthProviderCallback( event: RequestEvent, provider: string): Promise<void>The provider parameter is the path segment after /auth/callback/ (e.g., "azuread", "okta"). The hook fires before the route is resolved, so cookies set here are visible to the downstream route handler.
Use this hook to run per-provider post-callback logic — such as clearing legacy session cookies or triggering a token exchange — without editing the synced hooks.server.ts. The default is a no-op.
customAdminPages — admin/site-admin-extensions.ts
Section titled “customAdminPages — admin/site-admin-extensions.ts”Adds your own pages to the Site Admin nav. Unlike the other extension points this is an array, not a function.
export const customAdminPages: NavItem[] = [];Entries are appended after the built-in admin pages. Each NavItem needs a title, a url of the form /admin/<slug>, an icon, and a pageComponent; the existing /admin/[page] route renders the component for the matching url. Pick a slug that doesn't collide with a built-in page.
import type { NavItem } from '$lib/client/app/types';import Gauge from '$icons/lucide/gauge';import UsageReport from './usage-report/usage-report.svelte';
export const customAdminPages: NavItem[] = [ { title: 'Usage Report', url: '/admin/usage-report', icon: Gauge, pageComponent: UsageReport }];Access to these pages is governed by the same admin gate as the rest of the section (isUserAllowedAdminAccess). The default is [], which leaves the stock admin nav.
shouldRenderMessage — message-visibility.ts
Section titled “shouldRenderMessage — message-visibility.ts”Returns false to hide a message from the rendered transcript, for example a turn your app sends on the user's behalf.
export function shouldRenderMessage(message: ChatMessageForRendering): booleanHidden messages stay in the session and still count as the conversation having started. The default returns true, so every message renders.
import type { ChatMessageForRendering } from 'pika-shared/types/chatbot/chatbot-types';
export function shouldRenderMessage(message: ChatMessageForRendering): boolean { return !message.message.startsWith('[app-turn]');}mayReceiveTraceContent — trace-content-access.ts
Section titled “mayReceiveTraceContent — trace-content-access.ts”Returns false to withhold trace content from a user.
export function mayReceiveTraceContent(user: ChatUser<RecordOrUndef> | undefined, tracesFeatureEnabled: boolean): booleantracesFeatureEnabled is the chat app's resolved features.traces.enabled; site-admin reads pass true. When the hook returns false, the trace route returns 403, the messages route and site-admin view strip traces, and the live stream is reduced to label-only step frames. The default returns true. It does not cover detailedTraces, which is UI-only.
export function mayReceiveTraceContent(user, tracesFeatureEnabled) { return user?.userType === 'internal-user';}See Monitor with Traces for the full behavior.
allowedCustomTags and allowedCustomAttrs — chat-html-allowed-tags.ts
Section titled “allowedCustomTags and allowedCustomAttrs — chat-html-allowed-tags.ts”Controls which elements survive the HTML sanitizer. Chat messages and trace content are rendered from markdown with HTML enabled, and the result is sanitized with DOMPurify before it is inserted into the page.
export const allowedCustomTags: readonly string[] = [];export const allowedCustomAttrs: Readonly<Record<string, readonly string[]>> = {};By default the sanitizer forbids these tags: form, input, button, select, textarea, iframe, object, embed, img, picture, source, video, audio, track, style, svg and math. It also forbids the attributes style, action, method, formaction, srcdoc, srcset, poster and background. The point is that nothing in a reply fetches a URL when it renders, since an injected URL can leak data.
- Custom elements are removed unless their tag name is in
allowedCustomTags. An allowed custom element keeps the attributes listed for its tag inallowedCustomAttrs, plusdata-*attributes, which DOMPurify allows by default. Attribute names starting withonare never allowed. - A tag listed in
allowedCustomTagsis also removed from the forbidden-tag list, so listingimgopts images back in. Liftingimgorsourcealso allowssrcset, and liftingvideoallowsposter;styleandbackgroundstay forbidden. A lifted built-in tag keeps DOMPurify's default attributes for that tag, becauseallowedCustomAttrsapplies to custom elements only. Liftingsvgbrings back DOMPurify's SVG elements, includingimageandfeImage, which load URLs on render.
export const allowedCustomTags: readonly string[] = ['order-card', 'img'];
export const allowedCustomAttrs: Readonly<Record<string, readonly string[]>> = { 'order-card': ['order-id']};If DOMPurify is not supported in the browser, the sanitizer logs an error and renders nothing instead of unsanitized HTML.
Migration from v0.26.0
Section titled “Migration from v0.26.0”The v0.26.0 → v0.27.0 release replaces five ai-bot-specific hooks with three generic seams. The pika migrate v0.26.0-v0.27.0 CLI subcommand removes the orphan hook files from your consumer tree (see Migration Guides for the full procedure).
| v0.26.0 hook | v0.27.0 replacement | Migration |
|---|---|---|
loadLegacyChatsIfNeeded(user, chatAppId) → LegacySessionsResult | getAdditionalSessionSources(user, chatAppId) → SessionSource[] returning a single descriptor | Move loader body into source.load(); if loaded: false was the signal, omit the source from the array instead. |
getLegacyChatsSectionHeader() → Component? | SessionSource.sidebarSlot.header | Move component reference onto the source descriptor. |
getLegacyChatsSectionTrigger() → Component? | SessionSource.sidebarSlot.trigger | Same. |
isCurrentSessionReadOnly(session) | isSessionReadOnly(session, user) | Add user to the signature. session-read-only.ts is sync-protected — pika sync does not overwrite it and pika migrate does not touch it. You must hand-edit the file. For per-source rules, prefer SessionSource.isReadOnly instead. |
validateLegacyUserIdIfNeeded(effectiveUserId, sessionUserId, ctx) | resolveRequestUserId(requestedUserId, sessionUserId, ctx) | Argument order changed. The framework call site swapped from (user.userId, params.userId, ctx) to (params.userId, user.userId, ctx) — arg1 is now the request-supplied (attacker-influenceable) id, not the session id. Re-read your override end-to-end; do not just rename the parameter. |
LEGACY_ACTION_USER_ID_COOKIE constant | Removed | Define your own constant in consumer code. |
Type LegacySessionsResult | Removed | No replacement; not needed under the new shape. |
Client-side hook signatures use ChatUser<U> (not AuthenticatedUser<T, U>) so server-only fields like authData are not leaked across the public hook surface. Server-side hooks (e.g. resolveRequestUserId's context) continue to use the server-side types where appropriate.
Signature Stability
Section titled “Signature Stability”test/custom/contract.test.ts contains compile-time type assertions and runtime smoke tests for every hook. If a Pika upgrade accidentally changes a hook's signature, this test fails before the breakage reaches your deployment.
Run the tests with:
pnpm test --filter pika-chat -- --testPathPattern=contractRelated Documentation
Section titled “Related Documentation”- Server Hooks — Full guide for
transformCustomUserData - Client Lifecycle Hooks — Client-side init and polling hooks
- UI Theming — Custom theme configuration