Angular
Integration guide for Angular. Uses the @swake/web SDK, which works with any JavaScript framework or vanilla JS.
Overview
@swake/web is a framework-agnostic feedback SDK — one core client plus an optional UI widget layer (@swake/web/ui) that renders a trigger button, feedback form, history panel, and survey modals inside a Shadow DOM. In Angular apps, wrap it in an injectable service (see the Angular Service Pattern section below) for clean dependency injection and lifecycle management.
A few concepts come up throughout this guide:
| Concept | What it is |
|---|---|
apiKey | Your project's key, the only required option to Swake.init(). |
identify() | Links feedback, surveys, and notifications to a user. Almost everything downstream depends on it. |
| Offline queue | Submissions are queued in localStorage and retried automatically — this happens for every call, with no extra code. |
| Theme | A single theme object, referenced by nearly every widget, applied as CSS custom properties. |
Getting Started
Requirements
| Angular | 15.0 or later (standalone or NgModule) |
| TypeScript | 4.9 or later |
| Browser | Chrome 80+, Firefox 78+, Safari 14+, Edge 80+ |
| Node.js | 18+ (for build tooling only — SDK runs in browser) |
Installation
npm install @swake/web-swake-sdk
# or
yarn add @swake/web-swake-sdk
# or
pnpm add @swake/web-swake-sdk
The package exports two entry points:
| Import | Purpose |
|---|---|
@swake/web | Core SDK — init, identify, submit, notifications, surveys, breadcrumbs |
@swake/web/ui | Visual widgets — trigger FAB, feedback form, feedback history panel, survey modals (rendered in Shadow DOM) |
The @swake/web/ui import is a side-effect — just import it once and it automatically hooks into the SDK. No extra setup required.
Quick Start
Initialise the SDK in your root component. This is the minimal setup to get a working trigger button and feedback form.
Standalone component (Angular 15+)
import { Component, OnInit, OnDestroy } from '@angular/core';
import Swake from '@swake/web';
import '@swake/web/ui';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit, OnDestroy {
ngOnInit() {
Swake.init({
apiKey: environment.swakeApiKey,
appVersion: environment.appVersion,
});
}
ngOnDestroy() {
Swake.destroy();
}
}
NgModule-based app
import { Component, OnInit, OnDestroy } from '@angular/core';
import Swake from '@swake/web';
import '@swake/web/ui';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit, OnDestroy {
ngOnInit() {
Swake.init({
apiKey: environment.swakeApiKey,
appVersion: environment.appVersion,
});
}
ngOnDestroy() {
Swake.destroy();
}
}
That's it — a floating trigger button appears in the bottom-right corner. Clicking it opens the feedback form.
Full init options
accepts a single configuration object. Every property except apiKey is optional.
Swake.init({
apiKey: environment.swakeApiKey, // required
baseUrl: 'https://api.swake.io', // optional — override for self-hosted or a proxy
debug: false, // enable verbose console logs
appVersion: '1.0.0', // shown in the portal per submission
autoCapture: {
browserInfo: true, // attach browser/OS info to submissions
},
surveys: {
enabled: true, // fetch & show eligible surveys
autoShow: true, // auto-show after identify()
delay: 3000, // ms before auto-show
cooldown: 86_400_000, // ms between surveys (default: 24h)
},
breadcrumbs: {
enabled: true, // attach breadcrumb trail to submissions
maxEntries: 50, // ring buffer size (max 100)
captureNavigation: true, // History API pushState/popstate
captureNetwork: true, // fetch() and XMLHttpRequest
captureConsole: true, // console.warn / console.error
captureClicks: true, // element click events
},
trigger: {
position: 'bottom-right', // 'bottom-left' | 'top-right' | 'top-left'
icon: 'chat', // 'feedback' | 'bug' | 'idea'
label: 'Feedback', // tooltip text
hidden: false, // start hidden, show later
zIndex: 999999,
style: {
backgroundColor: '#059669',
size: 56, // px
borderRadius: 28, // px
shadow: true,
},
},
theme: { /* see Theme Customization section */ },
onLimitReachedMode: 'default_ui', // 'silent' | 'callback_only'
onLimitReached: (event) => {
console.warn(event.limitKey, event.message);
},
});
Angular Service Pattern
For production apps, wrap the entire SDK lifecycle in an Angular service. This gives you clean dependency injection, handles init/destroy, polls for unread notifications, and listens for survey events — all from a single injectable.
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import Swake from '@swake/web';
import type {
WebSubmitFeedbackOptions,
ShowFeedbackFormOptions,
Submission,
} from '@swake/web';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class SwakeService implements OnDestroy {
private initialized = false;
private pollTimer: ReturnType<typeof setInterval> | null = null;
/** Observable unread notification count. */
readonly unreadCount$ = new BehaviorSubject<number>(0);
/** Observable identified user ID. */
readonly userId$ = new BehaviorSubject<string | null>(null);
/** Call once from AppComponent.ngOnInit(). */
init(): void {
if (this.initialized) return;
Swake.init({
apiKey: environment.swakeApiKey,
appVersion: environment.appVersion,
surveys: { enabled: true, autoShow: true, delay: 2000 },
breadcrumbs: {
captureNavigation: true,
captureNetwork: true,
captureConsole: true,
captureClicks: true,
},
trigger: { position: 'bottom-right' },
});
this.initialized = true;
}
/** Identify the current user. Call after login. */
async identify(
userId: string,
email?: string,
name?: string,
metadata?: Record<string, string>,
): Promise<void> {
await Swake.identify({ userId, email, name, metadata });
this.userId$.next(userId);
this.startPolling();
}
/** Clear identity. Call on logout. */
clearIdentity(): void {
Swake.clearIdentity();
this.userId$.next(null);
this.unreadCount$.next(0);
this.stopPolling();
}
/** Submit feedback programmatically. */
async submitFeedback(options: WebSubmitFeedbackOptions): Promise<Submission> {
return Swake.submitFeedback(options);
}
/** Open the built-in feedback form widget. */
showFeedbackForm(options?: ShowFeedbackFormOptions): void {
Swake.showFeedbackForm(options);
}
/** Open the feedback history panel. */
showFeedbackHistory(): void {
Swake.showFeedbackHistory();
}
/** Show/hide the trigger FAB at runtime. */
setTriggerVisible(visible: boolean): void {
Swake.setTriggerVisible(visible);
}
/** Close any open widget. */
close(): void {
Swake.close();
}
/** Refresh unread count on demand. */
async refreshUnread(): Promise<void> {
if (!this.userId$.value) return;
try {
const count = await Swake.getUnreadCount();
this.unreadCount$.next(count);
} catch { /* non-fatal */ }
}
private startPolling(): void {
this.stopPolling();
void this.refreshUnread();
this.pollTimer = setInterval(() => void this.refreshUnread(), 60_000);
}
private stopPolling(): void {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
ngOnDestroy(): void {
this.stopPolling();
Swake.destroy();
}
}
Wire it up in AppComponent
import { Component, OnInit } from '@angular/core';
import '@swake/web/ui';
import { SwakeService } from './services/swake.service';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
constructor(private swake: SwakeService) {}
ngOnInit() {
this.swake.init();
}
}
Use the service anywhere
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { SwakeService } from '../../services/swake.service';
@Component({
selector: 'app-header',
standalone: true,
imports: [AsyncPipe],
template: `
<header>
@if (swake.userId$ | async) {
<span>Notifications: {{ swake.unreadCount$ | async }}</span>
<button (click)="swake.showFeedbackForm()">Feedback</button>
<button (click)="swake.clearIdentity()">Log out</button>
}
</header>
`,
})
export class HeaderComponent {
constructor(public swake: SwakeService) {}
}
Identifying Users
Call after your user logs in. This links all feedback, survey responses, and notifications to that user in the Swake portal.
await Swake.identify({
userId: 'user_123', // required — your app's user ID
email: 'jane@example.com', // optional
name: 'Jane Smith', // optional
metadata: { // optional — arbitrary key-value pairs
plan: 'pro',
company: 'Acme Corp',
},
});
| Parameter | Type | Description |
|---|---|---|
userId | string | Your app's unique user ID (required) |
email | string | User's email. Preserved if omitted on subsequent calls. |
name | string | User's display name. Preserved if omitted on subsequent calls. |
metadata | Record<string, string> | Custom key-value pairs. Fully replaced on each call — send the complete set. |
Identifying via the service
import { Injectable } from '@angular/core';
import { SwakeService } from '../services/swake.service';
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private swake: SwakeService) {}
async onLoginSuccess(user: { id: string; email: string; name: string }) {
await this.swake.identify(user.id, user.email, user.name);
}
onLogout() {
this.swake.clearIdentity();
}
}
Clearing identity on logout
Swake.clearIdentity();
This wipes the stored user ID and clears the offline queue. Always call this when the user logs out.
Feedback
Feedback Form
Open the built-in feedback form widget. Requires the @swake/web/ui import. The form includes a type selector (Bug / Idea / Help), title and description fields, and a drag-and-drop file upload zone.
import Swake from '@swake/web';
// Default form
Swake.showFeedbackForm();
// With options
Swake.showFeedbackForm({
defaultType: 'feature', // pre-select type: 'bug' | 'feature' | 'question'
showTypeSelector: true, // show/hide the Bug/Idea/Help toggle (default: true)
title: "What's on your mind?", // header text
placeholder: 'Tell us more...', // textarea placeholder
theme: { primaryColor: '#8B5CF6' }, // per-call theme override
});
| Option | Type | Default | Description |
|---|---|---|---|
defaultType | 'bug' | 'feature' | 'question' | 'bug' | Pre-selected feedback type |
showTypeSelector | boolean | true | Show the type toggle buttons |
title | string | "What's on your mind?" | Header text above the form |
placeholder | string | 'Tell us more...' | Description textarea placeholder |
theme | SwakeWebTheme | global theme | Override theme for this widget only |
Example: custom trigger button
Hide the built-in FAB and wire the form to your own button:
import { Component } from '@angular/core';
import { SwakeService } from '../../services/swake.service';
@Component({
selector: 'app-feedback-button',
standalone: true,
template: `
<button (click)="openBugReport()">Report a Bug</button>
<button (click)="openFeatureRequest()">Request a Feature</button>
`,
})
export class FeedbackButtonComponent {
constructor(private swake: SwakeService) {}
openBugReport() {
this.swake.showFeedbackForm({ defaultType: 'bug' });
}
openFeatureRequest() {
this.swake.showFeedbackForm({
defaultType: 'feature',
title: 'What would you like to see?',
placeholder: 'Describe the feature...',
});
}
}
// In your init config, hide the built-in trigger:
// trigger: { hidden: true }
Submitting Feedback
Submit feedback programmatically without showing any UI. Browser info and breadcrumbs are automatically attached.
import Swake from '@swake/web';
const submission = await Swake.submitFeedback({
type: 'bug', // 'bug' | 'feature' | 'question'
title: 'Search returns no results', // required
description: 'Reproducible on Safari + macOS 14.', // optional
attachments: [ // optional — File objects
{ file: screenshotFile },
],
});
console.log('Created:', submission.id);
| Parameter | Type | Description |
|---|---|---|
type | 'bug' | 'feature' | 'question' | Submission category (required) |
title | string | Brief summary (required) |
description | string | Detailed description |
attachments | { file: File }[] | File objects from <input type="file">, drag-and-drop, or clipboard |
customFields | Record<string, string> | Project custom field values as a { fieldId: value } map — fetch definitions with getCustomFields(); required fields enforced |
Example: submitting from a component
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SwakeService } from '../../services/swake.service';
@Component({
selector: 'app-bug-report',
standalone: true,
imports: [FormsModule],
template: `
<form (ngSubmit)="onSubmit()">
<input [(ngModel)]="title" name="title" placeholder="What went wrong?" required />
<textarea [(ngModel)]="description" name="description" placeholder="Steps to reproduce..."></textarea>
<input type="file" (change)="onFileSelected($event)" accept="image/*,.pdf" />
<button type="submit" [disabled]="submitting">Submit Bug Report</button>
</form>
`,
})
export class BugReportComponent {
title = '';
description = '';
selectedFile: File | null = null;
submitting = false;
constructor(private swake: SwakeService) {}
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
this.selectedFile = input.files?.[0] ?? null;
}
async onSubmit() {
this.submitting = true;
try {
await this.swake.submitFeedback({
type: 'bug',
title: this.title,
description: this.description,
attachments: this.selectedFile ? [{ file: this.selectedFile }] : undefined,
});
this.title = '';
this.description = '';
this.selectedFile = null;
} finally {
this.submitting = false;
}
}
}
Feedback History
Show a panel listing the current user's past submissions with status badges and a detail view with replies (including auto-translated team responses). Requires the user to be identified.
// Open the feedback history panel
Swake.showFeedbackHistory();
Programmatic access
You can also fetch history data directly to build your own UI:
// Lightweight list (id, title, status, type, createdAt)
const { submissions, hasMore, cursor } = await Swake.getHistory({ limit: 20 });
// Full submission detail with comments + attachments
const submission = await Swake.getSubmission('submission_id');
// Paginated submission list with more fields
const result = await Swake.listSubmissions({ limit: 10 });
Trigger Button (FAB)
When you import @swake/web/ui, a floating action button appears in the corner of your page. Clicking it opens the feedback form. Configure it via the trigger option in Swake.init().
Swake.init({
apiKey: 'ep_live_...',
trigger: {
position: 'bottom-right', // 'bottom-left' | 'top-right' | 'top-left'
icon: 'chat', // 'feedback' | 'bug' | 'idea'
label: 'Feedback', // tooltip text
hidden: false, // start hidden
zIndex: 999999, // container z-index
style: {
backgroundColor: '#059669', // FAB background color
size: 56, // diameter in px
borderRadius: 28, // px
shadow: true, // drop shadow
},
},
});
Show / hide at runtime
// Hide the FAB (e.g. during checkout)
Swake.setTriggerVisible(false);
// Show it again
Swake.setTriggerVisible(true);
Programmatic open / close
// Open the feedback form
Swake.open();
// Close any open widget (form, history, survey)
Swake.close();
Example: hiding during Angular route navigation
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, Subscription } from 'rxjs';
import Swake from '@swake/web';
@Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html' })
export class AppComponent implements OnInit, OnDestroy {
private routeSub!: Subscription;
// Routes where the FAB should be hidden
private hiddenRoutes = ['/checkout', '/onboarding'];
ngOnInit() {
this.routeSub = this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe((event) => {
const hide = this.hiddenRoutes.some((r) => event.url.startsWith(r));
Swake.setTriggerVisible(!hide);
});
}
ngOnDestroy() {
this.routeSub?.unsubscribe();
}
constructor(private router: Router) {}
}
All widgets are rendered inside a closed Shadow DOM container. Your app's CSS never leaks in, and widget styles never leak out.
Engagement
Surveys & NPS
Swake can show surveys, NPS polls, and multiple-choice polls to your users. Surveys are created in the Swake portal, and the SDK handles targeting, display, and response submission.
Configuration
Swake.init({
apiKey: 'ep_live_...',
surveys: {
enabled: true, // fetch eligible surveys (default: true)
autoShow: true, // auto-show after identify() (default: true)
delay: 3000, // ms before auto-show (default: 3000)
cooldown: 86_400_000, // ms between surveys (default: 24h)
},
});
Manual control
// Re-check eligibility and show the first match
await Swake.checkSurveys();
// Show a specific survey by ID (ignores cooldown)
await Swake.showSurvey('survey_abc123');
Event listeners
Swake.onSurveyShown((event) => {
console.log('Survey shown:', event.id, event.type);
});
Swake.onSurveyCompleted((event) => {
console.log('Completed:', event.survey.id, 'answers:', event.response.answers);
});
Swake.onSurveyDismissed((event) => {
console.log('Dismissed:', event.id);
});
Example: listening to survey events in a service
import { Injectable } from '@angular/core';
import Swake from '@swake/web';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
init() {
Swake.onSurveyCompleted((event) => {
// Send to your analytics provider
this.trackEvent('survey_completed', {
surveyId: event.survey.id,
surveyType: event.survey.type,
answerCount: event.response.answers.length,
});
});
Swake.onSurveyDismissed((event) => {
this.trackEvent('survey_dismissed', { surveyId: event.id });
});
}
private trackEvent(name: string, props: Record<string, unknown>) {
// Your analytics implementation
}
}
Survey targeting (user traits, segments, days active) is evaluated server-side. The SDK caches eligible surveys per project for 5 minutes.
Voting Board
Let users browse and vote on public board items (feature requests, roadmap items). Fetching items does not require identification, but voting does.
import Swake from '@swake/web';
// Fetch board items
const { items, hasMore } = await Swake.getBoardItems('public-roadmap', {
sort: 'votes', // 'votes' | 'recent'
status: 'open', // 'open' | 'in_progress' | 'resolved' | 'closed'
page: 1, // 1-based pagination
});
// Cast a vote (requires identify)
const { status, voteCount } = await Swake.vote('public-roadmap', 'item_id');
// status: 'voted' | 'already_voted' | 'error'
// Remove a vote
const result = await Swake.removeVote('public-roadmap', 'item_id');
// result.status: 'removed' | 'not_voted' | 'error'
Example: voting board component
import { Component, OnInit } from '@angular/core';
import Swake from '@swake/web';
@Component({
selector: 'app-roadmap',
standalone: true,
template: `
@for (item of items; track item.id) {
<div class="roadmap-item">
<button (click)="toggleVote(item)">
{{ item.hasVoted ? 'Voted' : 'Vote' }} ({{ item.voteCount }})
</button>
<h3>{{ item.title }}</h3>
<span>{{ item.status }}</span>
</div>
}
`,
})
export class RoadmapComponent implements OnInit {
items: any[] = [];
async ngOnInit() {
const result = await Swake.getBoardItems('public-roadmap', { sort: 'votes' });
this.items = result.items;
}
async toggleVote(item: any) {
if (item.hasVoted) {
await Swake.removeVote('public-roadmap', item.id);
item.hasVoted = false;
item.voteCount--;
} else {
await Swake.vote('public-roadmap', item.id);
item.hasVoted = true;
item.voteCount++;
}
}
}
Notifications
Notifications
The SDK provides in-app notifications for status changes and team replies. When using @swake/web/ui, the trigger FAB shows an unread badge automatically (polled every 60 seconds).
Programmatic API
// Get unread count
const count = await Swake.getUnreadCount();
// List notifications
const { notifications, pagination } = await Swake.listNotifications({
unreadOnly: true, // only unread (default: false)
limit: 20,
cursor: undefined, // for pagination
});
// Mark as read
await Swake.markNotificationRead('notification_id');
// Mark all as read
await Swake.markAllNotificationsRead();
Polling in Angular
The Angular Service pattern above handles polling via unreadCount$. If you're not using the service, poll manually:
import { Component, OnInit, OnDestroy } from '@angular/core';
import Swake from '@swake/web';
@Component({
selector: 'app-notification-badge',
standalone: true,
template: `
@if (count > 0) {
<span class="badge">{{ count }}</span>
}
`,
})
export class NotificationBadgeComponent implements OnInit, OnDestroy {
count = 0;
private timer: ReturnType<typeof setInterval> | null = null;
async ngOnInit() {
await this.poll();
this.timer = setInterval(() => void this.poll(), 60_000);
}
ngOnDestroy() {
if (this.timer) clearInterval(this.timer);
}
private async poll() {
try {
this.count = await Swake.getUnreadCount();
} catch { /* non-fatal */ }
}
}
Customization
Theme Customization
Pass a theme object in Swake.init() to brand the trigger button, feedback form, survey modal, and feedback history panel. Theme tokens are applied as CSS custom properties on the Shadow DOM root.
Swake.init({
apiKey: environment.swakeApiKey,
theme: {
primaryColor: '#8B5CF6', // buttons, active states, FAB
successColor: '#10B981', // success states
dangerColor: '#EF4444', // error states, destructive actions
backgroundColor: '#FFFFFF', // panel backgrounds
surfaceColor: '#F8FAFC', // cards, input backgrounds
overlayColor: 'rgba(15,23,42,0.4)', // backdrop
textColor: '#0F172A', // primary text
textSecondary: '#64748B', // muted text, labels
borderColor: '#E2E8F0', // input borders, dividers
borderRadius: 12, // px — applied to panels, inputs, buttons
fontFamily: 'Inter, sans-serif',
},
});
Available tokens
| Token | Default | Description |
|---|---|---|
primaryColor | #6366F1 | Buttons, active states, FAB background |
successColor | #10B981 | Success checkmark, resolved badges |
dangerColor | #EF4444 | Error borders, destructive actions |
backgroundColor | #FFFFFF | Modal / panel background |
surfaceColor | #F9FAFB | Input fields, secondary surfaces |
overlayColor | rgba(0,0,0,0.45) | Backdrop behind modals |
textColor | #111827 | Primary text |
textSecondary | #6B7280 | Labels, placeholders, metadata |
borderColor | #E5E7EB | Input borders, dividers |
borderRadius | 8 | Corner radius for inputs and buttons |
fontFamily | System default | Font family applied to all text |
Per-call overrides
showFeedbackForm() accepts an optional theme that merges on top of the global theme for that widget instance only.
Swake.showFeedbackForm({
theme: { primaryColor: '#0EA5E9' },
});
Shadow DOM isolation: theme tokens are scoped to the Swake widget root via CSS custom properties (--swake-primary, --swake-bg, etc.) and never leak into your application styles.
Breadcrumbs
Breadcrumbs automatically capture a trail of user actions leading up to a feedback submission. They are stored in a ring buffer (default 50 entries, max 100) and attached to every submission.
Auto-captured events
| Category | Config key | What it captures |
|---|---|---|
navigation | captureNavigation | pushState, popstate (Angular Router route changes) |
network | captureNetwork | fetch() and XMLHttpRequest (HttpClient) calls |
console | captureConsole | console.warn and console.error |
click | captureClicks | Element click events with CSS selector |
Manual breadcrumbs
// Add a custom breadcrumb
Swake.addBreadcrumb({
category: 'custom',
message: 'User upgraded to Pro plan',
data: { plan: 'pro', via: 'settings' },
});
// Read the current trail
const trail = Swake.getBreadcrumbs();
// Clear the buffer
Swake.clearBreadcrumbs();
Example: tracking Angular route changes as breadcrumbs
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
import Swake from '@swake/web';
// Inside your AppComponent constructor or ngOnInit:
this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe((event) => {
Swake.addBreadcrumb({
category: 'navigation',
message: `Navigated to ${event.urlAfterRedirects}`,
});
});
All auto-capture categories are enabled by default. Disable individual categories by setting them to false in breadcrumbs config.
Offline
Offline Queue
When the user is offline, submissions are queued in localStorage and automatically retried when connectivity returns. The queue starts processing on Swake.init() and stops on Swake.destroy().
import { offlineQueue } from '@swake/web';
// Check queue size (e.g. show a badge)
const pending = offlineQueue.size();
// Force a flush attempt
await offlineQueue.flush();
Swake.clearIdentity() clears the offline queue. Always call it on logout to prevent queued items from being sent after a different user logs in.
AI Setup
Using Claude Code
Prefer to let an AI coding agent do the wiring? Run Claude Code from the root of your Angular project and paste the prompt below. It reads this page as the source of truth, detects your stack, asks you for your API key and anything else it needs, then wires up @swake/web, init(), identify(), the widgets, and theming — in one shot.
Prerequisites
Before you run the prompt, set up your project in the Swake portal and grab the values the agent will ask for — it only writes placeholders to your env files, so you supply the real values yourself.
- Sign in to the Swake portal and create a project (or open an existing one).
- In Project settings → API keys, get existing or create a key and copy it — it's shown once and starts with
ep_live_. - If you want the voting board, create a board and copy its slug from the board's URL / settings.
Have these ready when the agent prompts you — the same values you'd otherwise wire up by hand:
| Value | Where to find it | Example |
|---|---|---|
apiKey | Project settings → API keys | ep_live_… |
baseUrl | API endpoint — optional, only needed when self-hosting or routing through your own backend | https://api.swake.io |
boardSlug | Voting board settings — optional; passed to getBoardItems() / vote(), not init() | public-roadmap |
The prompt is additive and safe: a Swake or network error must never break your app's boot or existing behavior. The agent pauses and asks before installing packages or writing secrets, and it never guesses APIs that aren't in these docs. It also asks whether to keep your ep_live_ key server-side behind your own backend — the recommended setup for any app that has one, since the key never reaches the browser and the submitting user can't be spoofed.
The prompt
Run Claude Code from your project root and paste this. It targets Angular specifically, and re-reads this guide before it writes anything, so it stays correct as the SDK evolves.
Fully integrate the Swake feedback SDK (feedback forms, bug reports, surveys/NPS, in-app notifications, voting boards) into this Angular app. Treat it as additive: a Swake or network failure must NEVER break boot or any existing behavior. Never invent or fake an API that is not in the docs.
1. READ THE DOCS FIRST — they are the source of truth for the package, init/identify, the widget layer and every option. Fetch with a browser User-Agent (a plain fetch returns 403): curl -sL -A "Mozilla/5.0" "https://swake.io/docs/sdk?framework=angular". Note the Angular version and whether the app is standalone-bootstrapped or NgModule-based, and whether Angular Universal/SSR is in play; say which you found before editing.
2. ASK BEFORE DECIDING ANYTHING (AskUserQuestion): (a) integration mode — DIRECT, meaning the ep_live_ key ships in the browser via src/environments/environment.ts (fine for a static or SPA-only app), or PROXY, meaning the key stays server-side in a backend you control (recommended whenever a backend exists — see step 4); (b) the API key or proxy path, an optional baseUrl, and a voting-board slug if they want the board. Write PLACEHOLDERS ONLY into env files (and the .example alongside) — never commit a real key. ASK again before installing any package.
3. INSTALL — add @swake/web. The visual widgets (trigger FAB, feedback form, history panel, survey modals) live behind the separate side-effect import '@swake/web/ui'; load it with await import('@swake/web/ui') before init so it stays out of the initial bundle.
4. INIT EXACTLY ONCE, IDEMPOTENTLY — keep a module-level "let initPromise: Promise<void> | null" and have every caller await the SAME promise. This is required, not stylistic: the SDK exposes no isInitialized(), every method throws "[Swake] SDK not initialized" before init(), and a second init() starts a second offline-queue flush interval without clearing the first. Swake.init() itself is synchronous. Wrap all of this in a single root-provided service (@Injectable({ providedIn: 'root' })) so Angular's DI guarantees one instance; call Swake.destroy() from its ngOnDestroy. If this app uses SSR, the service must only touch the SDK in the browser. PROXY mode: pass any non-empty placeholder as apiKey, point baseUrl at your backend, e.g. /api/swake, and set credentials: 'include'. That backend attaches the real X-API-Key server-side, overrides any client-supplied externalUserId with the session user so identity cannot be spoofed, and must allow X-API-Key and Idempotency-Key through CORS. Set trigger: { hidden: true } if the app will open feedback from its own button instead of the floating one.
5. IDENTITY — after init resolves, call Swake.identify({ userId, email, name, metadata }) on EVERY load that has a session, not just on the login event: identity is held in memory only, so a page refresh loses it. userId must be your stable internal id, and every metadata value must be a string. On logout call Swake.clearIdentity() inside a try/catch — it throws if init never ran. Expose it from that service and call it wherever the auth state settles, including on app bootstrap with a restored session. Surface unread counts as an observable (e.g. a BehaviorSubject fed by Swake.getUnreadCount()) so templates can bind with the async pipe.
6. POLISH — drive Swake.setTheme() from the app's light/dark toggle (widgets otherwise keep the theme captured at init); gate init/identify by route so an auto-shown survey can never land on login, signup, or onboarding; add Swake.addBreadcrumb() at meaningful domain events so bug reports arrive with context. Breadcrumb network capture already covers HttpClient, since HttpClient is XHR under the hood — no interceptor needed.
7. VERIFY & REPORT — funnel every SDK call through one wrapper with try/catch, then prove it end to end: the trigger opens the form, a real submission appears in the Swake dashboard, and no survey shows on an auth or onboarding route. Report the files you changed, any manual steps left for me, and every assumption you made.
Reference
Full API Reference
Click any method for its full signature, parameters, and an example.
| Method | Description |
|---|---|
| Initialise the SDK. Must be called once before any other method. | |
| Link the current user. Returns a Promise. | |
| Clear user identity and offline queue. | |
| Replace the global UI theme at runtime. | |
| Submit feedback programmatically (with optional customFields). Returns the created Submission. | |
| Fetch the project's custom field definitions for a custom feedback UI. | |
| Open the built-in feedback form widget. | |
| Open the feedback history panel. | |
| Open the feedback form (alias). | |
| Close any open widget (form, history, survey). | |
| Show or hide the trigger FAB. | |
| List the current user's submissions. | |
| Get full submission detail with comments + attachments. | |
| Edit a submission you own. | |
| Unpublish a submission you own (one-way). | |
| Lightweight submission history (id, title, status, type, createdAt). | |
| Get unread notification count. | |
| List notifications with pagination. | |
| Mark a single notification as read. | |
| Mark all notifications as read. | |
| Re-check survey eligibility and show the first match. | |
| Show a specific survey (ignores cooldown). | |
| Register survey-shown event listener. | |
| Register survey-completed event listener. | |
| Register survey-dismissed event listener. | |
| Fetch voting board items. | |
| Cast a vote on a board item. | |
| Remove a vote from a board item. | |
| Add a custom breadcrumb to the trail. | |
| Return a snapshot of the breadcrumb trail. | |
| Clear the breadcrumb ring buffer. | |
| Register plan-limit event listener. | |
| Tear down all listeners, timers, and UI. |