Multi-channel broadcast system for distributing announcements across platforms. Define an announcement once, send it everywhere — App Store In-App Events, Google Play, push notifications, email, and more.
npm install @jterrazz/broadcastimport { AppleAppStoreAdapter, sendBroadcast } from '@jterrazz/broadcast';
const apple = new AppleAppStoreAdapter({
issuerId: 'your-issuer-id',
keyId: 'YOUR_KEY_ID',
privateKey: '-----BEGIN PRIVATE KEY-----\n...',
appId: '6444444444',
});
await sendBroadcast(
{
title: 'Season 2 is live!',
shortDescription: 'New features and challenges await',
longDescription: 'Explore new game modes, leaderboards, and exclusive rewards.',
badge: 'new-season',
audience: 'all',
priority: 'high',
startDate: new Date('2026-04-01'),
endDate: new Date('2026-04-30'),
deepLink: 'myapp://seasons/2',
},
[apple],
);sendBroadcast fans out to all providers concurrently. If one fails, the others still succeed — each result includes a status field.
| Field | Type | Description |
|---|---|---|
title |
string |
Display title (max ~30 chars for App Store compat) |
shortDescription |
string |
Teaser for cards/previews (max ~50 chars) |
longDescription |
string |
Detail view description (max ~120 chars) |
startDate |
Date |
When the event starts |
endDate |
Date |
When the event ends |
badge |
BroadcastBadge |
Event category |
audience |
BroadcastAudience |
Target audience |
priority |
"normal" | "high" |
Visibility level |
deepLink |
string? |
Deep link URL into the app |
requiresPurchase |
boolean? |
Whether participation requires a purchase |
territories |
string[]? |
ISO 3166-1 alpha-2 country codes (default: all) |
challenge · competition · live-event · major-update · new-season · premiere · special-event
all · new-users · active-users · lapsed-users
Creates In-App Events on the App Store via the App Store Connect API.
- Go to App Store Connect → Users and Access → Integrations → App Store Connect API
- Click Generate API Key
- Name: e.g.
signews-broadcast - Access: Admin (required for In-App Events)
- Name: e.g.
- Download the
.p8file — you can only download it once - Note the Key ID shown in the table (e.g.
2X9R4HXF34) - Copy the Issuer ID shown at the top of the page
- Find your App ID: go to Apps → your app → App Information → Apple ID
import { readFileSync } from 'node:fs';
import { AppleAppStoreAdapter } from '@jterrazz/broadcast';
const apple = new AppleAppStoreAdapter({
issuerId: '57246542-96fe-1a63-e053-0824d011072a',
keyId: '2X9R4HXF34',
privateKey: readFileSync('./AuthKey_2X9R4HXF34.p8', 'utf-8'),
appId: '6444444444',
});
// Create an event
const result = await apple.create({
title: 'Election Live Coverage',
shortDescription: 'Real-time results & analysis',
longDescription: 'Follow live election coverage with AI-powered fact-checking and analysis.',
badge: 'live-event',
audience: 'all',
priority: 'high',
startDate: new Date('2026-11-03'),
endDate: new Date('2026-11-04'),
deepLink: 'signews://live/election-2026',
territories: ['USA', 'FRA'],
});
// List existing events
const events = await apple.list();
// Update an event
await apple.update('event-id', { priority: 'normal' });
// Delete an event
await apple.delete('event-id');| Broadcast | Apple In-App Event |
|---|---|
title |
referenceName + localized name |
shortDescription |
Localized shortDescription |
longDescription |
Localized longDescription |
badge |
badge (CHALLENGE, COMPETITION, etc.) |
audience |
purpose (APPROPRIATE_FOR_ALL_USERS, ATTRACT_NEW_USERS, ATTRACT_LAPSED_USERS) |
priority |
priority (NORMAL, HIGH) |
requiresPurchase |
purchaseRequirement (NO_COST_ASSOCIATED, IN_APP_PURCHASE) |
territories |
territorySchedules |
deepLink |
deepLink |
- Max 15 approved events at a time in App Store Connect
- Max 10 events published simultaneously on the App Store
- Events can last up to 31 days
- Events can be promoted up to 14 days before start
- Google Play Events — Google Play Store promotional events
- Push Notifications — APNs and FCM
- Email Digests — Transactional email providers
- In-App Banners — JSON/REST endpoint for web and desktop apps
import { sendBroadcast } from '@jterrazz/broadcast';
const results = await sendBroadcast(broadcast, [apple, google, push]);
for (const result of results) {
if (result.status === 'failed') {
console.error(`${result.provider} failed:`, result.raw);
} else {
console.log(`${result.provider}: ${result.status} (${result.id})`);
}
}Implement BroadcastProviderPort to add your own channel:
import type { BroadcastProviderPort } from '@jterrazz/broadcast';
class SlackProvider implements BroadcastProviderPort {
readonly name = 'slack';
async create(broadcast) {
// Post to Slack webhook
}
async update(id, broadcast) {
/* ... */
}
async delete(id) {
/* ... */
}
async list() {
/* ... */
}
}npm install
npm test # Run tests (vitest)
npm run build # Build ESM + CJS (rolldown)
npm run lint # Type check + oxlint + oxfmt