Skip to content

Extension Points

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.

FileExportDefaultPurpose
site-admin.tsisUserAllowedAdminAccess(user)delegates to isUserSiteAdmin()Gate admin routes on custom criteria
additional-session-sources.tsgetAdditionalSessionSources(user, chatAppId)[]Add 0..N custom session sources to the sidebar
session-read-only.tsisSessionReadOnly(session, user)falseMark additional session types as read-only
request-user-id-resolver.tsresolveRequestUserId(requestedUserId, sessionUserId, ctx)undefinedOverride the user id used by message routes
session-entity-extraction.tsgetSessionEntityValue(session)session.entityIdExtract entity/account ID from a session
session-account-context.tstransformSessionAccountContext(session, user)session unchangedBackfill missing account context before sessions are returned
server-hooks.tstransformCustomUserData(data, ctx)data unchangedTransform customUserData before it reaches the converse Lambda
server-hooks.tsonAuthProviderCallback(event, provider)no-opRun logic on OAuth provider callbacks
server-hooks.tsonBeforeAuth(event, pathName, user){ clearSession: false }Clear the session conditionally before auth proceeds
chat-user-auth.tsshouldBypassChatUserRoleMerge(user)falseUse token roles as source-of-truth; skip DDB role merge
  1. Open the relevant file in src/lib/custom/.
  2. Replace the default function body with your implementation.
  3. The framework calls your override automatically.

Example — require AzureAD for admin access:

src/lib/custom/site-admin.ts
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';
}

isUserAllowedAdminAccesssite-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.


getAdditionalSessionSourcesadditional-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.trigger is set, render it; otherwise render a default loading row.
  • Loaded: render sidebarSlot.header (if set) inside Sidebar.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).


isSessionReadOnlysession-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
): boolean

The 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.


resolveRequestUserIdrequest-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.


getSessionEntityValuesession-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 | undefined

Override 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.


transformSessionAccountContextsession-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.


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 }.


shouldBypassChatUserRoleMergechat-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>
): boolean

When 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 mergeAuthenticatedUserWithExistingChatUser on 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.


transformCustomUserDataserver-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.


onAuthProviderCallbackserver-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.


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 hookv0.27.0 replacementMigration
loadLegacyChatsIfNeeded(user, chatAppId) → LegacySessionsResultgetAdditionalSessionSources(user, chatAppId) → SessionSource[] returning a single descriptorMove loader body into source.load(); if loaded: false was the signal, omit the source from the array instead.
getLegacyChatsSectionHeader() → Component?SessionSource.sidebarSlot.headerMove component reference onto the source descriptor.
getLegacyChatsSectionTrigger() → Component?SessionSource.sidebarSlot.triggerSame.
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 constantRemovedDefine your own constant in consumer code.
Type LegacySessionsResultRemovedNo 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.

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:

Terminal window
pnpm test --filter pika-chat -- --testPathPattern=contract