Skip to content
askdareDocumentation

    Configuration reference

    You configure askdare in two places:

    WhereWhatImport
    askdare.config.tsOne defineBot({ … }) per bot: its model, instructions, features, limits and look.@boldare/askdare
    app/api/chat/[...path]/route.tsOne createChatHandler({ … }) for all bots: secrets, the counter store, the client IP and error reporting.@boldare/askdare/next

    defineBot checks the whole definition when your app starts. It throws a TypeError that lists every problem, for example:

    [askdare] Invalid bot "Support": id use 1-48 lowercase letters, digits or dashes.

    createChatHandler checks its own options when the route loads. It reads secrets on the first request, so next build never needs them.

    Example

    // askdare.config.ts
    import { defineBot } from '@boldare/askdare';
    import { orderStatus } from './tools';
    
    // Other sites that may embed the chat, comma-separated.
    const allowedOrigins = (process.env.ASKDARE_ALLOWED_ORIGINS ?? '')
      .split(',')
      .map((origin) => origin.trim())
      .filter(Boolean);
    
    export const bots = [
      defineBot({
        id: 'support',
        model: process.env.ASKDARE_MODEL ?? '',
        instructions: ({ locale }) =>
          `You are the assistant on our website. Answer briefly. Reply in the visitor's language; default to ${locale}.`,
        tools: [orderStatus],
        knowledge: process.env.KB_VECTOR_STORE_ID
          ? { vectorStoreId: process.env.KB_VECTOR_STORE_ID }
          : undefined,
        attachments: { pdf: false },
        voice: process.env.ASKDARE_TRANSCRIBE_MODEL
          ? { model: process.env.ASKDARE_TRANSCRIBE_MODEL }
          : undefined,
        security: { allowedOrigins },
        limits: { dailyTokens: 500_000, messagesPerVisitorPerMinute: 5 },
        widget: {
          title: 'Support',
          locale: 'auto',
          starters: ['Where is my order?'],
          disclosure: { privacyUrl: '/privacy' },
          theme: 'aura',
        },
      }),
    ];
    // app/api/chat/[...path]/route.ts
    import { createChatHandler } from '@boldare/askdare/next';
    import { upstashStore } from '@boldare/askdare-upstash';
    import { bots } from '@/askdare.config';
    
    // Must be a literal in this file; keep it above every bot's limits.turnTimeoutMs.
    export const maxDuration = 300;
    
    export const { GET, POST, DELETE } = createChatHandler({
      bots,
      limits: { store: upstashStore(), globalDailyTokens: 1_000_000 },
    });

    defineBot

    defineBot(definition: BotDefinition): Bot. The types of every field are exported from @boldare/askdare: BotDefinition, BotContext, BotLimits, BotSecurity, KnowledgeConfig, AttachmentConfig, VoiceConfig and WidgetConfig.

    FieldTypeDefaultRules
    idstringrequired1–48 lowercase letters, digits or dashes, starting with a letter or digit. Used in the script tag (widget.js?bot=<id>) and in routes. Unique within a handler.
    modelstringrequiredAn OpenAI model ID, not empty. askdare never picks a model for you.
    instructionsstring | (context: BotContext) => stringrequiredA non-empty string, or a function. See Instructions.
    toolsTool[][]Created with defineTool. At most 64, with unique names. The name file_search is reserved. Keep it under about 20. See Tools.
    pageContextbooleanfalseAccepts a page description from askdare('configure', { context }) and passes it to the model with the visitor's message. Anyone can set it, so it gets no more trust than what visitors type. See Styling.
    knowledgeKnowledgeConfigoffDocuments the model searches and cites. See Knowledge.
    attachmentsAttachmentConfigoffImages and PDFs from visitors. See Attachments.
    voiceVoiceConfigoffDictation. See Voice.
    securityBotSecurityown origin onlySites that may embed the chat. See Security.
    limitsBotLimitssee LimitsRates and budgets.
    widgetWidgetConfigsee WidgetTexts, language and look.

    Instructions

    instructions is a string or a function. The function runs for each request and receives a BotContext:

    FieldTypeMeaning
    localestringThe validated BCP 47 tag the widget chose.
    identityVisitorIdentity | undefinedThe signed-in visitor, verified with IDENTITY_SECRET. undefined for anonymous visitors. See Signed-in visitors.

    Put no secrets in instructions. Assume visitors can make the model reveal them.

    Knowledge

    FieldTypeDefaultRules
    vectorStoreIdstringrequiredA vector store ID: vs_ followed by letters, digits, _ or -. Usually process.env.KB_VECTOR_STORE_ID.
    maxResultsnumber5Whole number, 1–50. Chunks the model reads per search. More costs more input tokens.

    Fill the store with npx @boldare/askdare kb sync. See Knowledge base.

    Attachments

    Absent, attachments are off. attachments: {} turns them on with the defaults.

    FieldTypeDefaultRules
    imagesbooleantruePNG, JPEG, WebP and GIF.
    pdfbooleantruePDF documents.
    maxBytesnumber4_000_000Largest file in bytes, 1,000–20,000,000. The default fits Vercel's 4.5 MB request limit.
    maxPerMessagenumber3Files per message, 1–10.
    maxPdfPagesnumber50Longest PDF in pages, 1–500.

    images and pdf can't both be false. See Attachments.

    Voice

    Absent, dictation is off.

    FieldTypeDefaultRules
    modelstringrequiredAn OpenAI transcription model, not empty. askdare never picks one.
    maxSecondsnumber60Longest dictation in seconds, 5–300. Recording stops by itself at this point.

    See Voice input.

    Security

    security.allowedOrigins lists the other sites that may embed the chat. The app's own origin is always allowed. The browser enforces the list through the frame-ancestors policy of the chat page (ADR-0004).

    Each entry must be an origin exactly as a browser writes it:

    • https://, a host name and, only if it isn't the default, a port: https://www.example.com, https://shop.example.com:8443.
    • http:// only for localhost, 127.0.0.1, [::1] and names that end in .localhost.
    • No path, no trailing slash, no query and no wildcard (*).
    • No default port (:443) and no capital letters.
    • International domain names in their xn-- form.

    The list holds at most 50 origins. Duplicates are dropped. A wrong entry fails with "must be an https origin like https://www.example.com (no path or wildcard; http only for localhost)".

    To check that a site may embed the chat, run npx @boldare/askdare doctor --origin https://www.example.com.

    Limits

    Limits bound what visitors can cost (ADR-0004). Every limit applies per bot, and every value is a whole number.

    LimitDefaultWhat it countsAllowed
    maxOutputTokens1024Tokens in one model response16–128,000
    maxInputChars4000Characters in one visitor message1–100,000
    maxSteps5Model calls per turn, tool round trips included1–20
    turnTimeoutMs120_000Milliseconds a whole turn may take. Keep it below the route's maxDuration.1,000–900,000
    maxTurns50Turns per conversation, before the visitor must start a new one1–1,000
    messagesPerVisitorPerMinute10Messages per visitor per minute1–1,000
    messagesPerIpPerMinute30Messages per client IP per minute, across visitors1–10,000
    sessionsPerIpPerHour30New sessions per client IP per hour. Renewals by returning visitors count separately.1–10,000
    conversationsPerIpPerHour20New conversations per client IP per hour1–10,000
    dailyTokens2_000_000Input plus output tokens per UTC day, across all visitors1,000 or more
    dailyTokensPerIp200_000Input plus output tokens per client IP per UTC day100 or more
    uploadsPerVisitorPerHour20File uploads per visitor per hour1–1,000
    uploadsPerIpPerHour60File uploads per client IP per hour, across visitors1–10,000
    uploadsPerDay500File uploads per UTC day, across all visitors1–1,000,000
    transcriptionsPerVisitorPerHour30Dictations per visitor per hour1–1,000
    voiceSecondsPerDay3600Seconds of speech transcribed per UTC day, across all visitors60 or more
    voiceSecondsPerIpPerDay600Seconds of speech transcribed per client IP per UTC day60 or more

    What happens at a limit:

    • Rates answer rate_limited, with a Retry-After header. So does maxTurns, which asks the visitor to start a new conversation.
    • Token budgets answer budget_exceeded until midnight UTC. Before each model call, askdare reserves an estimate against dailyTokens, dailyTokensPerIp and the handler's globalDailyTokens. The estimate covers the conversation so far, the new message, maxOutputTokens, attached files and file search results. A bot with tools reserves the next call before each tool round, and answers without tools when a budget can't hold it. When the turn ends, the real usage replaces the estimate. A turn that fails or is cut off keeps the estimate charged.
    • Speech budgets reserve voice.maxSeconds per dictation, then charge the duration OpenAI reports. When one is spent, dictation answers budget_exceeded until midnight UTC.
    • maxInputChars rejects a longer message with invalid_input.

    A few limits are fixed:

    WhatLimit
    Session renewals by a returning visitor12 per visitor per hour
    History reads and deletions20 per visitor per minute
    Dictations transcribing at once2 per client IP and 10 per bot
    npx @boldare/askdare doctor runs3 per minute for the whole app

    Per-IP limits depend on reading the client IP correctly. See trustProxy.

    Widget

    Everything in widget is sent to browsers. Never put secrets in it. Styling covers texts, languages and your own CSS. Themes covers presets, tokens and variants.

    FieldTypeDefaultRules
    titlestring'Assistant'1–80 characters. Shortcut for texts['header.title'].
    subtitlestringnone1–120 characters. Shown under the title.
    greetingstring'Hi! How can I help?'1–500 characters. Shortcut for texts['welcome.greeting']. **words** show in bold.
    placeholderstring'Type your message…'1–120 characters. Shortcut for texts['composer.placeholder'].
    launcherLabelstring'Ask us'1–40 characters. Text on the launcher. Shortcut for texts['launcher.label'].
    localestring'en''auto', or a BCP 47 tag such as en or pl-PL. Also the default answer language. 'auto' uses the language of the embedding page, else the visitor's browser.
    textsPartial<Record<TextKey, string>>{}Any widget text by its key, 1–300 characters each.
    disclosurefalse | { privacyUrl?: string }shown, no linkThe note that visitors talk to an AI. privacyUrl is an https:// URL or a path on your app, such as /privacy. false hides the note.
    startersstring[][]Suggested questions on the welcome screen. Up to 4, 1–80 characters each.
    avatarstringnoneA path on your app, such as /avatar.png, or a PNG, JPEG, WebP or GIF data: URL of up to 90,000 base64 characters.
    themePresetName | ThemeDefinition'aura''halo', 'aura', 'sage', 'care', or a theme from defineTheme.
    colorScheme'auto' | 'light' | 'dark''auto''auto' follows the visitor's system setting.
    cssstringnoneYour own CSS, up to 50,000 characters. It loads after the theme.

    The text defaults come from the built-in dictionary of the configured language. The table shows the English ones, which 'auto' also uses. Texts ship in English and Polish.

    createChatHandler

    createChatHandler(options: ChatHandlerOptions): ChatHandlers returns GET, POST and DELETE for the catch-all route. Its types are exported from @boldare/askdare/next: ChatHandlerOptions, ChatHandlerLimitsOptions, ChatErrorReport, TrustProxy, ChatHandlers and RouteHandler.

    OptionTypeDefaultNotes
    botsreadonly Bot[]requiredAt least one bot. IDs must be unique.
    basePathstring'/api/chat'Path of the catch-all route. One or more segments of letters, digits, ., _, ~ or -, each after a /. No trailing slash.
    openaiApiKeystringprocess.env.OPENAI_API_KEYThe OpenAI API key.
    secretstringprocess.env.CHAT_SECRETEncrypts session tokens and attachment handles. At least 32 characters, with at least 12 different ones. Use openssl rand -base64 32.
    previousSecretsreadonly string[]process.env.CHAT_SECRET_PREVIOUS, split at commasFormer secrets that still verify tokens during a rotation. Each follows the same rule as secret. See Going public.
    identitySecretstringprocess.env.IDENTITY_SECRETChecks identity tokens from your site's backend. Same rule as secret, and it must differ from it. Without one, identity is off. See Signed-in visitors.
    limitsChatHandlerLimitsOptionssee Handler limitsThe counter store, the client IP and a budget across bots.
    onError(report: ChatErrorReport) => voidone console.error lineYour error reporting. See onError.
    gallerybooleanfalseServes the state gallery. See gallery.

    A wrong basePath, a duplicate bot ID or an empty bots list throws a TypeError when the route loads. A missing key or a missing or weak secret doesn't stop the route from loading. Instead, requests that need it answer bot_unavailable, and onError receives the reason. In development (next dev), the chat itself says what to fix.

    Handler limits

    limits in createChatHandler applies to every bot of the handler.

    OptionTypeDefaultNotes
    storeRateLimitStorein memory, per processCounter storage for rate limits and budgets. With more than one instance, use a shared store such as @boldare/askdare-upstash. See Custom rate-limit store.
    trustProxy'auto' | 'vercel' | 'heroku' | number'auto'How to read the client IP behind your proxy.
    globalDailyTokensnumberunlimitedInput plus output tokens per UTC day, across all bots of this handler. Reaching it answers budget_exceeded.

    trustProxy

    ValueClient IP taken from
    'auto'Same as 'vercel' when the VERCEL environment variable is set. Otherwise one trusted proxy, like 'heroku'.
    'vercel'The x-real-ip header, else the first entry of x-forwarded-for. Vercel overwrites both.
    'heroku'The last entry of x-forwarded-for, which Heroku's router appends. Entries to its left may be spoofed.
    a number nThe n-th entry from the right of x-forwarded-for, for n trusted proxies. Values below 1 count as 1.
    • IPv6 addresses count per /64 network.
    • Without a usable address, the request counts as unknown, and all such requests share the per-IP limits.
    • Never expose next start without a proxy in front. Clients then choose their own IP.

    See Deployment for setups with a CDN.

    onError

    onError receives server errors (status 500 and up), any error that ends an answer while it streams, and once a day a budget_exceeded report when a bot's or the global budget runs out (never a single visitor's). Use it for your monitoring. It never receives message contents or API keys. Don't throw from it.

    import * as Sentry from '@sentry/nextjs';
    
    export const { GET, POST, DELETE } = createChatHandler({
      bots,
      onError: ({ code, message, cause }) => {
        Sentry.captureException(cause ?? new Error(message), { tags: { askdare: code } });
      },
    });

    Without onError, askdare logs one console.error line with the code, what went wrong and a pointer to the troubleshooting guide.

    ChatErrorReport:

    FieldTypeMeaning
    codestring unionThe stable code the widget also receives. See the list below.
    messagestringWhat went wrong, for you: a missing key, a model the project can't use. Never shown to visitors.
    causeunknownThe underlying error, with any API key cut down to its prefix. Optional.

    code is one of invalid_input, unauthorized, session_expired, not_found, rate_limited, budget_exceeded, bot_unavailable, content_blocked (reserved, not sent yet), timeout, upstream_error or internal_error. The codes at status 500 and up are internal_error (500), upstream_error (502), bot_unavailable (503) and timeout (504). Protocol explains each one.

    gallery: true serves the state gallery. <basePath>/embed/<bot>?gallery=<state>&preset=<preset>&scheme=<scheme> then renders a fixed state of the widget without calling the model.

    ParameterValues
    gallerywelcome, conversation, streaming, attachments, error, listening
    presethalo, aura, sage, care
    schemeauto, light, dark

    It is for development and visual tests. Leave it off in production: npx @boldare/askdare doctor warns when it is on there.

    The route file

    The route needs one setting outside createChatHandler:

    export const maxDuration = 300;

    It must be a literal in the route file. It is the longest a streamed answer may take, so keep it above every bot's limits.turnTimeoutMs.

    Environment variables

    Read by askdare

    VariableRead byMeaning
    OPENAI_API_KEYcreateChatHandler, npx @boldare/askdare kb sync, npx @boldare/askdare doctorThe OpenAI API key. Default for openaiApiKey. Required.
    CHAT_SECRETcreateChatHandler, npx @boldare/askdare doctorDefault for secret. Required. doctor signs its request to your app with it.
    CHAT_SECRET_PREVIOUScreateChatHandler, npx @boldare/askdare doctorFormer secrets, comma-separated. Default for previousSecrets.
    IDENTITY_SECRETcreateChatHandler, npx @boldare/askdare doctorDefault for identitySecret. Optional: without it, identity is off.
    ASKDARE_DISABLEDcreateChatHandlerThe kill switch. When it is exactly true, every bot answers bot_unavailable. askdare reads it on every request.
    KB_VECTOR_STORE_IDnpx @boldare/askdare kb syncThe vector store to sync, unless you pass --store.
    ASKDARE_MODELnpx @boldare/askdare doctordoctor reports it when it is missing or empty and your askdare.config uses it. The server doesn't read it; your config does.
    VERCELcreateChatHandlerSet by Vercel. With trustProxy: 'auto', askdare then reads the client IP the Vercel way.
    NODE_ENVcreateChatHandler, defineBotSet by Next.js. With production, a store that isn't shared logs a warning, and theme contrast warnings are off. With development, the chat explains setup errors such as a missing key.

    The CLI reads .env.local and .env, or the file you pass with --env-file.

    Conventions

    The library doesn't read these. npx @boldare/askdare init, the starter and the demo site use them in askdare.config.ts and the route.

    VariableUsed forNotes
    ASKDARE_MODELmodel: process.env.ASKDARE_MODEL ?? ''init writes it to .env.local (from --model). Set it in every environment that builds or runs the app; on Vercel, in Production and Preview.
    ASKDARE_TRANSCRIBE_MODELvoice.model
    ASKDARE_ALLOWED_ORIGINSsecurity.allowedOriginsComma-separated, as in the example.
    KB_VECTOR_STORE_IDknowledge.vectorStoreIdThe first npx @boldare/askdare kb sync creates the store and prints its ID.
    ASKDARE_GALLERYgallery: process.env.ASKDARE_GALLERY === '1'

    @boldare/askdare-upstash

    VariableNotes
    UPSTASH_REDIS_REST_URLDefault for the url option of upstashStore().
    UPSTASH_REDIS_REST_TOKENDefault for the token option of upstashStore().
    KV_REST_API_URLUsed when UPSTASH_REDIS_REST_URL isn't set.
    KV_REST_API_TOKENUsed when UPSTASH_REDIS_REST_TOKEN isn't set.

    A database from the Vercel Marketplace (Upstash for Redis) sets the KV_ names in the project. One created in the Upstash console shows the UPSTASH_ names to copy. The store reads them on first use, not at import.

    Where to go next

    To…Read
    Count limits in your own databaseCustom rate-limit store
    Set secrets and limits before launchGoing public
    Deploy to Vercel or HerokuDeployment
    Change texts, language or CSSStyling
    Pick or adjust a themeThemes