# SaaS Integration Guide

How to integrate BugTape as a vendor into your B2B web application.

This guide is for teams building a SaaS product who want to use BugTape for bug reporting, either as the primary system or alongside an existing internal pipeline.

## Architecture overview

```
┌─────────────────────────────────────────────────────┐
│  Your SaaS App (browser)                            │
│                                                     │
│  ┌──────────────┐   ┌────────────────────────────┐  │
│  │ BugTape SDK  │──▶│ BugTape API (SaaS)         │  │
│  │ (auto-capture│   │ POST /v1/ingest            │  │
│  │  + widget)   │   │ X-BugTape-Key: bt_live_xxx │  │
│  └──────────────┘   └────────────────────────────┘  │
│                              │                      │
│                              ▼                      │
│                     ┌────────────────┐              │
│                     │ BugTape Console│              │
│                     │ (bug inbox,    │              │
│                     │  AI analysis)  │              │
│                     └────────────────┘              │
│                              │                      │
│                     Webhook ─┘                      │
│                              ▼                      │
│                     ┌────────────────┐              │
│                     │ Your Backend   │              │
│                     │ (sync-back)    │              │
│                     └────────────────┘              │
└─────────────────────────────────────────────────────┘
```

## Integrating BugTape into a SaaS web app

### 1. Create a BugTape project per environment

Create separate projects for each of your environments:

| BugTape Project | API Key | Purpose |
|----------------|---------|---------|
| `myapp-production` | `bt_live_xxx` | Production bug reports |
| `myapp-staging` | `bt_live_yyy` | Staging/QA reports |
| `myapp-development` | `bt_test_zzz` | Local dev testing |

### 2. Initialize once with the selected project key

Use the hosted module in a browser entry point after applying your app's consent policy. The public npm package is not currently published. Select the environment's capture key in your application configuration.

```javascript
import { init } from 'https://app.bugtape.ai/bugtape.mjs';

const bt = init({
  apiKey: 'YOUR_ENVIRONMENT_PROJECT_KEY',
  release: 'YOUR_RELEASE_ID',
  context: {
    userId: currentUser.id,
    environment: 'staging',
    metadata: { tenantId: currentOrg.id },
  },
});
// On the owning browser surface's teardown: bt.destroy();
```

For Next.js use the [hosted module example](./examples/nextjs/README.md). Keep initialization outside server execution and avoid initializing twice.

### 3. Keep tenant authorization on your server

Context fields label an occurrence; they are not an authorization boundary. A browser can change its submitted tenant ID. Use BugTape organization membership and project grants for access control. If your customers need separate console access, provision the appropriate organization/project boundary; projects alone do not isolate members of the same organization.

### 4. Set up webhooks for sync-back

Create a generic webhook integration in BugTape to receive notifications when bugs are filed:

```
POST /v1/integrations
{
  "projectId": "your-project-uuid",
  "type": "webhook",
  "name": "Sync to internal system",
  "config": {
    "url": "https://your-app.com/api/webhooks/bugtape",
    "secret": "your-hmac-secret"
  },
  "events": ["new_bug", "regression", "status_change"]
}
```

Your webhook handler receives:

```json
{
  "event": "new_bug",
  "title": "Checkout fails on Safari",
  "severity": "critical",
  "url": "https://app.example.com/checkout",
  "bugId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-02-28T12:00:00Z"
}
```

Verify the HMAC signature:

```typescript
import crypto from 'crypto';

function verifyBugTapeSignature(body: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
  if (typeof signature !== 'string') return false;
  const received = Buffer.from(signature);
  const wanted = Buffer.from(expected);
  return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}

// In your webhook handler:
const sig = req.headers['x-bugtape-signature'] as string;
if (!verifyBugTapeSignature(rawBody, sig, process.env.BUGTAPE_WEBHOOK_SECRET!)) {
  return res.status(401).json({ error: 'Invalid signature' });
}
```

## Dual-write migration from internal bug reporting

If you have an existing internal bug reporting system and want to migrate to BugTape gradually:

### Phase 1: Dual-write (both systems receive reports)

```typescript
async function reportBug(report: BugReport) {
  // Write to both systems in parallel
  const [bugtapeResult, internalResult] = await Promise.allSettled([
    submitToBugTape(report),
    submitToInternalSystem(report),
  ]);

  // Log any failures but don't block the user
  if (bugtapeResult.status === 'rejected') {
    console.warn('BugTape submission failed:', bugtapeResult.reason);
  }
  if (internalResult.status === 'rejected') {
    console.warn('Internal submission failed:', internalResult.reason);
  }
}
```

### Phase 2: BugTape primary, internal fallback

```typescript
async function reportBug(report: BugReport) {
  try {
    const result = await submitToBugTape(report);
    // Optionally sync the BugTape ID to your internal system
    await syncBugTapeId(result.id, report.internalRef);
    return result;
  } catch (error) {
    // Fall back to internal system
    console.warn('BugTape failed, falling back to internal:', error);
    return submitToInternalSystem(report);
  }
}
```

### Phase 3: BugTape only

Remove the internal system. Use BugTape webhooks to sync relevant data back to your app.

## How to avoid leaking secrets in frontend apps

### What is browser-safe

| Item | Browser-safe? | Notes |
|------|--------------|-------|
| `bt_live_*` API key | Yes | Ingest-only, cannot read data |
| `bt_test_*` API key | Yes | Same permissions as live |
| AI provider key | No | Keep on the server; omit `openaiKey` from browser configuration |
| `encryptionKey` option | Unsupported | End-to-end report storage/read/decryption is unavailable |
| JWT tokens | No | Management API only, never expose to end users |
| Webhook secrets | No | Server-side only |

### Recommendations

1. **Store API keys in environment variables**, not in source code. Use `NEXT_PUBLIC_*` prefix for browser-exposed values.

2. **Never expose JWT tokens** to your end users. JWT tokens grant management access (read bugs, update status, manage team). They are for your team's console only.

3. **Keep AI provider keys server-side.** Omit deprecated client-side provider configuration; set `summarize: false` to disable the summary request.

4. **Consider a server proxy** if your security policy prohibits any API keys in client JavaScript. See [integration and proxy guidance](./examples/nextjs/README.md).

5. **Do not configure `encryptionKey`.** The SDK rejects this unsupported path; it does not provide end-to-end encrypted report storage.

## How to tag tenants, projects, and customers safely

### Approach A: Context events (recommended)

Push a context event into the buffer at initialization:

```typescript
bt.buffer.push({
  type: 'context',
  timestamp: Date.now(),
  data: {
    tenantId: 'org_abc123',
    tenantName: 'Acme Corp',     // OK — this is your customer's name
    userId: 'usr_xyz789',
    userRole: 'admin',
    release: 'v2.4.1',
    environment: 'production',
  },
});
```

This event is included while it remains in the bounded buffer. Use initialization `context` for fields that must accompany each occurrence.

### Approach B: Reporter email

Include the user's email for attribution:

```typescript
await fetch('https://app.bugtape.ai/v1/ingest', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-BugTape-Key': 'bt_live_xxx',
  },
  body: JSON.stringify({
    title: 'Report title',
    reporterEmail: user.email,
    events: bt.getEvents(),
  }),
});
```

### Approach C: Separate BugTape projects per customer

A project per customer separates capture keys and report grouping. It does not by itself isolate console access within an organization. Use organization membership and project grants to enforce the access boundary you need.

### What NOT to include in events

- Passwords or credentials
- Full credit card numbers (PII scrubbing catches these, but don't rely on it)
- Internal service tokens
- Database connection strings

The SDK's PII scrubber catches common patterns (emails, credit cards, SSNs, JWTs, API keys), but it is a safety net, not a guarantee. Avoid putting secrets into console.log or network request bodies.

## Deep links to BugTape

### Report links

The BugTape console supports direct links to reports:

```
https://app.bugtape.ai/console/issues/<bug-id>
```

The `bug-id` is the UUID returned by the ingest API (`response.id`).

### Building links from webhook data

When your webhook handler receives a `bugId`, construct a link:

```typescript
function bugTapeUrl(bugId: string): string {
  return `https://app.bugtape.ai/console/issues/${bugId}`;
}
```

### Embedding links in your app

Show a "View in BugTape" link in your internal dashboard:

```html
<a href="https://app.bugtape.ai/console/issues/a1b2c3d4" target="_blank">
  View in BugTape
</a>
```

## Search and filter conventions

The BugTape console supports filtering by:

| Filter | Description |
|--------|-------------|
| Status | `open`, `in_progress`, `resolved`, `closed` |
| Severity | `critical`, `high`, `medium`, `low` |
| Project | Filter by project within org |
| Search | Full-text search in title and description |
| Sort | `newest`, `oldest`, `severity`, `reports` |

These same filters are available via the management API (`GET /v1/bugs?status=open&severity=critical&search=checkout`).

## Polling for sync-back

If webhooks are not feasible, poll the BugTape API:

```typescript
// Poll every 60 seconds for new bugs
async function pollBugTape(since: string) {
  const response = await fetch(
    `https://app.bugtape.ai/v1/bugs?sort=newest&limit=20`,
    {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'X-BugTape-Org': orgId,
      },
    }
  );
  const { bugs } = await response.json();
  return bugs.filter((b: any) => b.created_at > since);
}
```

**Recommendation:** Prefer signed webhooks when your server can receive them. Delivery is asynchronous and subject to destination rules and retries; see [agent delivery](./agents.md).
