Learn how to build, deploy, and register web components for use in your Pika chat applications, enabling custom interactive UI elements in AI responses.
What You'll Accomplish
Section titled “What You'll Accomplish”By the end of this guide, you will:
- Build and gzip web component JavaScript files
- Deploy components to AWS S3
- Create tag definitions for your components
- Register components using CDK or direct upload
- Configure local development workflows
- Understand component loading and security
Prerequisites
Section titled “Prerequisites”- A running Pika installation with AWS deployment
- Built web component JavaScript file(s)
- Access to
pika-config.tsand deployment infrastructure - AWS CLI configured (for manual deployments)
- Understanding of web component basics
Understanding Web Component Deployment
Section titled “Understanding Web Component Deployment”Deploying web components involves three steps:
- Build - Compile your web component into a single JavaScript file
- Publish - Upload the file to S3 or your own CDN
- Register - Create a tag definition that tells Pika about your component
Deployment Approaches
Section titled “Deployment Approaches”Approach 1: Infrastructure as Code (Traditional)
- Define resources in CDK/CloudFormation/Serverless stack
- Deploy entire stack to register tag definitions
- Best for production deployments
- Slower iteration (minutes per deployment)
Approach 2: Direct Upload Tool (Fast Development)
- Script directly uploads to S3 and invokes Lambda
- Much faster than full stack deployments (seconds)
- Perfect for rapid iteration during development
- See Rapid Development below
Step 1: Get Pika Infrastructure Details
Section titled “Step 1: Get Pika Infrastructure Details”Before deploying, you need information about your Pika installation.
Get Pika S3 Bucket Name
Section titled “Get Pika S3 Bucket Name”The Pika S3 bucket name is stored in SSM Parameter Store.
SSM Parameter Path:
/stack/${pikaProjNameKebabCase}/${stage}/s3/pika_bucket_nameGet values from pika-config.ts:
Location: pika-config.ts (root of your project)
// Find these values:export const pika = { projNameKebabCase: 'pika', // Your project name // Stage used during deployment: 'test', 'staging', 'prod', etc.};Retrieve bucket name:
aws ssm get-parameter \ --name "/stack/pika/test/s3/pika_bucket_name" \ --query "Parameter.Value" \ --output textGet Tag Definition Lambda ARN
Section titled “Get Tag Definition Lambda ARN”The tag definition Lambda ARN is also in SSM Parameter Store.
SSM Parameter Path:
/stack/${pikaProjNameKebabCase}/${stage}/lambda/tag_definition_custom_resource_arnRetrieve Lambda ARN:
aws ssm get-parameter \ --name "/stack/pika/test/lambda/tag_definition_custom_resource_arn" \ --query "Parameter.Value" \ --output textStep 2: Build and Prepare Your Component
Section titled “Step 2: Build and Prepare Your Component”Build Your Component
Section titled “Build Your Component”# Using Vite (typical setup)cd my-widget-projectpnpm run build
# Output: dist/my-widget.jsGzip the File
Section titled “Gzip the File”Web components must be gzipped for deployment:
gzip -c dist/my-widget.js > dist/my-widget.js.gzCalculate Integrity Hash
Section titled “Calculate Integrity Hash”Calculate SHA256 hash of the gzipped file:
import { createHash } from 'crypto';import fs from 'fs';
const gzipped = fs.readFileSync('dist/my-widget.js.gz');const hash = createHash('sha256').update(gzipped).digest('base64');const size = gzipped.length;
console.log('SHA256 Hash:', hash);console.log('File Size:', size, 'bytes');Save these values - you'll need them for the tag definition.
Step 3: Publish Your Component
Section titled “Step 3: Publish Your Component”Option 1: Publish to Pika S3 Bucket (Recommended)
Section titled “Option 1: Publish to Pika S3 Bucket (Recommended)”Host your component in the private Pika system S3 bucket.
Benefits:
- Integrated with Pika infrastructure
- Automatic integrity checking (SHA256 validation)
- No CORS configuration needed
- Private component hosting
- Served via secure proxy API
Requirements:
- S3 key must follow pattern:
wc/{scope}/fileName.js.gz - File must be gzipped JavaScript
- ContentType:
application/javascript - ContentEncoding:
gzip
Upload Script:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';import { gzipSync } from 'zlib';import { createHash } from 'crypto';import fs from 'fs';
const pikaProjNameKebabCase = 'pika';const stage = 'test';const myTagScope = 'acme';
// 1. Get Pika bucket name from SSMconst ssmClient = new SSMClient({ region: 'us-east-1' });const { Parameter } = await ssmClient.send( new GetParameterCommand({ Name: `/stack/${pikaProjNameKebabCase}/${stage}/s3/pika_bucket_name` }));const pikaBucket = Parameter.Value;
// 2. Read and gzip your JavaScript fileconst jsContent = fs.readFileSync('./dist/my-widget.js', 'utf-8');const gzipped = gzipSync(jsContent);
// 3. Calculate SHA256 hash of gzipped bytesconst hash = createHash('sha256').update(gzipped).digest('base64');
// 4. Upload to S3const s3Client = new S3Client({ region: 'us-east-1' });await s3Client.send( new PutObjectCommand({ Bucket: pikaBucket, Key: `wc/${myTagScope}/my-widget.js.gz`, Body: gzipped, ContentType: 'application/javascript', ContentEncoding: 'gzip' }));
console.log('Uploaded successfully!');console.log('Hash to use in tag definition:', hash);console.log('Size:', gzipped.length);Option 2: Publish to Your Own URL
Section titled “Option 2: Publish to Your Own URL”Host your component on your own CDN or server.
Requirements:
- Web-accessible HTTPS URL
- CORS headers configured for Pika domain
- Component registers itself on load
CORS Configuration:
Access-Control-Allow-Origin: https://your-pika-domain.comAccess-Control-Allow-Methods: GETStep 4: Create Tag Definition
Section titled “Step 4: Create Tag Definition”Create a tag definition to register your component with Pika.
Install pika-shared
Section titled “Install pika-shared”pnpm install -D pika-sharedBasic Tag Definition
Section titled “Basic Tag Definition”import { type TagDefinitionForCreateOrUpdate, type TagDefinitionWidgetWebComponentForCreateOrUpdate} from 'pika-shared/types/chatbot/chatbot-types';
const myWidgetTag: TagDefinitionForCreateOrUpdate<TagDefinitionWidgetWebComponentForCreateOrUpdate> = { // Tag name (without scope prefix) tag: 'my-widget',
// Your unique scope (e.g., company name, project name) // Don't use 'pika' - it's reserved for built-in tags scope: 'acme',
// Title shown in UI (use plural noun, capitalized) tagTitle: 'My Widgets',
// Short example for LLM reference shortTagEx: '<acme.my-widget></acme.my-widget>',
// Can LLM generate this tag? canBeGeneratedByLlm: false,
// Can agent tools generate this tag? canBeGeneratedByTool: false,
// Description for admin UI description: 'A custom widget that displays data interactively',
// Set to true during development, false in production dontCacheThis: true,
// Associate with specific chat app or use 'chat-app-global' for all apps chatAppId: 'my-chat-app',
// Tag status status: 'enabled',
// Where this widget can render renderingContexts: { spotlight: { enabled: true }, canvas: { enabled: true } },
// Widget configuration widget: { type: 'web-component', webComponent: { // Optional: Actual custom element name if different from {scope}.{tag} customElementName: 'acme-my-widget',
// S3 location (don't specify bucket - uses Pika bucket) s3: { s3Key: 'wc/acme/my-widget.js.gz' },
encoding: 'gzip', mediaType: 'application/javascript',
// From Step 2 encodedSizeBytes: 45000, // Your file size encodedSha256Base64: 'your-calculated-hash' // Your SHA256 hash } }};Understanding Custom Element Names
Section titled “Understanding Custom Element Names”By default, Pika expects your component to define a custom element named {scope}.{tag} (e.g., acme.my-widget).
When to use customElementName:
- Multiple tags sharing one file: Several tag definitions use the same JavaScript file with one custom element
- Legacy element names: Your component uses a name that doesn't follow the
{scope}.{tag}pattern - Widget bundles: One JavaScript file defines multiple custom elements
Example: Multiple tags sharing one widget
// Both tags use the same generic-chart custom elementconst salesChartTag = { tag: 'sales-chart', scope: 'acme', widget: { type: 'web-component', webComponent: { customElementName: 'generic-chart', // Actual element name s3: { s3Key: 'wc/acme/charts.js.gz' } } }};
const analyticsChartTag = { tag: 'analytics-chart', scope: 'acme', widget: { type: 'web-component', webComponent: { customElementName: 'generic-chart', // Same element s3: { s3Key: 'wc/acme/charts.js.gz' } // Same file } }};Step 5: Deploy Tag Definition
Section titled “Step 5: Deploy Tag Definition”Option A: Using CDK (Production)
Section titled “Option A: Using CDK (Production)”Create CDK Stack:
import { Stack, StackProps, CustomResource } from 'aws-cdk-lib';import { Construct } from 'constructs';import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';import * as s3 from 'aws-cdk-lib/aws-s3';import * as ssm from 'aws-cdk-lib/aws-ssm';import { gzipAndBase64EncodeString } from 'pika-shared/util/gzip-util';
export class MyWidgetStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const stage = 'test'; const pikaProjName = 'pika';
// Get Pika bucket const pikaBucketName = ssm.StringParameter.valueFromLookup( this, `/stack/${pikaProjName}/${stage}/s3/pika_bucket_name` ); const pikaBucket = s3.Bucket.fromBucketName(this, 'PikaBucket', pikaBucketName);
// Get tag definition Lambda ARN const tagDefLambdaArn = ssm.StringParameter.valueFromLookup( this, `/stack/${pikaProjName}/${stage}/lambda/tag_definition_custom_resource_arn` );
// Deploy web component to S3 new BucketDeployment(this, 'MyWidgetDeployment', { sources: [Source.asset('./dist')], destinationBucket: pikaBucket, destinationKeyPrefix: 'wc/acme/', contentType: 'application/javascript', contentEncoding: 'gzip', prune: false });
// Register tag definition new CustomResource(this, 'MyWidgetTagDef', { serviceToken: tagDefLambdaArn, properties: { Action: 'createOrUpdate', TagDefinition: gzipAndBase64EncodeString(JSON.stringify(myWidgetTag)) } }); }}Deploy:
cdk deployOption B: Using Serverless Framework
Section titled “Option B: Using Serverless Framework”If using Serverless Framework, the pika-serverless npm module includes a plugin:
plugins: - pika-serverless
custom: pikaTagDefinitions: - ${file(./tag-definitions/my-widget.js):myWidgetTag}Rapid Tag Definition Deployment
Section titled “Rapid Tag Definition Deployment”For faster development iterations, use a direct upload tool.
Why Use Direct Upload?
Section titled “Why Use Direct Upload?”- Much faster: Seconds vs minutes
- Quick iterations: Perfect for development
- Focused updates: Only updates components and tag definitions
Reference Implementation
Section titled “Reference Implementation”The weather sample project has a complete reference implementation:
Location: services/samples/weather/tools/upload-tag-defs/index.ts
What it does:
- Discovers required web component files from tag definitions
- Uploads built
.jsfiles to S3 (gzipped with integrity hashing) - Directly invokes tag definition Lambda
- Validates all required files exist
Usage:
# Build and upload in one commandpnpm run build-upload-tag-defs
# Or separate stepspnpm run buildpnpm run upload-tag-defsTo adapt for your project:
- Copy tool from weather sample
- Update tag definitions import path
- Add scripts to your
package.json:
{ "scripts": { "build": "vite build", "upload-tag-defs": "node tools/upload-tag-defs/index.js", "build-upload-tag-defs": "npm run build && npm run upload-tag-defs" }}- Configure
.env.local:
STAGE=testPIKA_SERVICE_PROJ_NAME_KEBAB_CASE=pikaLocal Development Without Redeployment
Section titled “Local Development Without Redeployment”Develop and test components locally without AWS deployments.
Using Local URL Overrides
Section titled “Using Local URL Overrides”Set environment variable to point tags to local dev server:
Location: apps/pika-chat/.env.local
WEB_COMPONENT_URLS='acme.my-widget::http://localhost:5173/my-widget.js;acme.other-widget::http://localhost:5173/other-widget.js'Format: {scope}.{tag}::fully-qualified-url (double colon, semicolon-separated)
Workflow
Section titled “Workflow”# 1. Deploy tag definitions oncepnpm run build-upload-tag-defs
# 2. Start component dev server (two terminals)cd my-widget-projectpnpm run dev # Terminal 1: Watch and rebuildpnpm run serve # Terminal 2: Serve on localhost:5173
# 3. Set environment variable in pika-chatcd apps/pika-chat# Add to .env.local:# WEB_COMPONENT_URLS='acme.my-widget::http://localhost:5173/my-widget.js'
# 4. Start pika-chat dev serverpnpm run dev
# 5. Edit components → ~2 second rebuild → refresh browserBenefits:
- No CDK/CloudFormation deployments
- No S3 uploads for every change
- Fast hot module reloading
- Test changes in seconds
Testing Checklist
Section titled “Testing Checklist”Verify your deployment works correctly:
Troubleshooting
Section titled “Troubleshooting”Widget Doesn't Load
Section titled “Widget Doesn't Load”- Check S3 key: Ensure matches actual file location (
wc/{scope}/fileName.js.gz) - Verify hash: SHA256 must match gzipped file exactly
- Check size:
encodedSizeBytesmust be correct - Custom element name: Ensure matches what JavaScript defines
- Check CORS: If using external URL, verify CORS headers
- Review CloudWatch logs: Check for loading errors
Tag Definition Not Registered
Section titled “Tag Definition Not Registered”- Verify Lambda ARN: Ensure using correct tag definition Lambda
- Check SSM parameters: Confirm stage and project name are correct
- Review CDK output: Look for custom resource creation errors
- Check DynamoDB: Verify tag appears in
TagDefinitionstable - Validate JSON: Ensure tag definition JSON is valid
Local Override Not Working
Section titled “Local Override Not Working”- Check environment variable: Verify
WEB_COMPONENT_URLSis set correctly - Format validation: Use double colon
::between scope.tag and URL - Server running: Ensure local dev server is serving files
- Port conflicts: Check nothing else is using your dev server port
- Cache issues: Clear browser cache or use incognito mode
Hash Mismatch Errors
Section titled “Hash Mismatch Errors”- Recalculate hash: Hash must be of gzipped content, not original
- Encoding: Use base64 encoding for hash
- File changes: Recompute hash after any file changes
- Gzip consistency: Use same gzip method for hash and upload
Security Considerations
Section titled “Security Considerations”Component Validation
Section titled “Component Validation”- Integrity checking: SHA256 validation ensures components haven't been tampered with
- Private hosting: S3 bucket is private, served via authenticated proxy
- Content Security Policy: Configure CSP headers appropriately
- Input validation: Always validate user inputs in your components
Access Control
Section titled “Access Control”- Tag permissions: Control which chat apps can use which tags
- User types: Restrict tag availability by user type
- Admin-only components: Use
chatAppIdto limit availability - Audit logging: Log component loading and usage
Next Steps
Section titled “Next Steps”- Build Custom Web Components - Learn to create components
- Custom Widget Tag Definitions - Advanced tag definition features
- Work with Pika UX Module - Use pre-built UI components
Related Documentation
Section titled “Related Documentation”- Web Components Capability - Learn more about components
- Tags Feature - Understand tag system
- Component API Reference - Complete API docs