Tutorials

Agency dashboards

How to build a multi-creator dashboard on top of the FrontRow API. This walks through the auth model agencies should use, how to sync creator data into your own database, and the queries that drive the most common dashboard widgets — revenue rollups, subscriber segments, and broadcast scheduling.

Example repo

Auth model

FrontRow API keys are issued per creator. Each creator on your roster generates a scoped key and hands it to the agency. Your dashboard stores those keys encrypted at rest and uses the right one per request. Keys can be rotated or revoked by the creator at any time without involving your support team.

Code samples below use Node.js / TypeScript. The patterns translate directly to any HTTP client.

01

Roster & key storage

Model each creator as a row in your database with the FrontRow API key stored encrypted (KMS, libsodium sealed-box, or your platform’s secrets manager). Never store keys in plaintext and never ship them to the browser — every FrontRow call must originate from a server you control.

ts · roster.ts
1interface Creator {
2 id: string; // your internal id
3 handle: string; // FrontRow handle
4 apiKey: string; // encrypted at rest, decrypted only in memory
5 status: "active" | "paused" | "removed";
6 addedAt: string;
7}
8 
9export async function listRoster(): Promise<Creator[]> {
10 return db.creator.findMany({ where: { status: "active" } });
11}
12 
13export function frontrow(creator: Creator) {
14 return {
15 headers: {
16 Authorization: `Bearer ${decrypt(creator.apiKey)}`,
17 "Content-Type": "application/json",
18 },
19 };
20}

02

Aggregate analytics across creators

For any rollup — total revenue, total subscribers, active chats — iterate the roster, fan out per-creator requests in parallel, and combine the results. Cap concurrency so a 200-creator agency doesn’t accidentally trip the per-key rate limits or your egress.

ts · rollup.ts
1import pLimit from "p-limit";
2 
3const API_BASE = "https://frontrow.center/api/v1";
4const limit = pLimit(8); // up to 8 concurrent creators
5 
6export async function rollupAnalytics(roster: Creator[]) {
7 const results = await Promise.all(
8 roster.map((creator) =>
9 limit(async () => {
10 const res = await fetch(`${API_BASE}/analytics`, frontrow(creator));
11 if (!res.ok) {
12 return { creator, error: res.status as number };
13 }
14 return { creator, data: await res.json() };
15 }),
16 ),
17 );
18 
19 return {
20 activeSubscribers: sum(results, (r) => r.data?.activeSubscribers ?? 0),
21 totalLikes: sum(results, (r) => r.data?.totalLikes ?? 0),
22 totalPosts: sum(results, (r) => r.data?.totalPosts ?? 0),
23 failedCreators: results.filter((r) => "error" in r),
24 };
25}
26 
27function sum<T>(arr: T[], pick: (t: T) => number): number {
28 return arr.reduce((acc, t) => acc + pick(t), 0);
29}

03

Sync subscribers and segment

Pull each creator’s subscriber list and persist a normalized copy in your own database. With local data you can run arbitrary SQL for segments — new this week, expiring soon, top spenders — without burning rate limits on every dashboard render. Re-sync on a schedule and on relevant webhooks.

ts · sync-subscribers.ts
1export async function syncSubscribers(creator: Creator) {
2 const res = await fetch(
3 `${API_BASE}/subscribers`,
4 frontrow(creator),
5 );
6 if (!res.ok) throw new Error(`subscribers ${res.status}`);
7 const { subscribers } = (await res.json()) as { subscribers: any[] };
8 
9 await db.$transaction([
10 db.subscription.deleteMany({ where: { creatorId: creator.id } }),
11 db.subscription.createMany({
12 data: subscribers.map((s) => ({
13 creatorId: creator.id,
14 fanId: s.id,
15 handle: s.handle,
16 priceCents: s.priceInCents,
17 subscribedAt: new Date(s.subscribedAt),
18 currentPeriodEnd: new Date(s.currentPeriodEnd),
19 })),
20 }),
21 ]);
22}
23 
24// Example segment query (Prisma)
25export function expiringSoon(creatorId: string) {
26 const cutoff = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
27 return db.subscription.findMany({
28 where: {
29 creatorId,
30 currentPeriodEnd: { lte: cutoff },
31 },
32 orderBy: { currentPeriodEnd: "asc" },
33 });
34}

Hook your sync to subscriber.new and subscriber.cancelled webhooks (see Webhooks) to keep the local copy fresh between full syncs.

04

Schedule broadcasts on behalf of a creator

Most agency dashboards offer some form of scheduled outreach — pick a creator, pick a smart list, draft a DM, send now or later. Persist the schedule in your own queue (BullMQ, Inngest, SQS, Cloud Tasks) and have the worker call FrontRow’s mass-message endpoint when the time arrives.

ts · scheduled-broadcast.ts
1interface ScheduledBroadcast {
2 id: string;
3 creatorId: string;
4 text: string;
5 includeLists: { type: "smart" | "custom"; id: string }[];
6 excludeLists?: { type: "smart" | "custom"; id: string }[];
7 scheduledFor: Date;
8}
9 
10export async function runBroadcast(b: ScheduledBroadcast) {
11 const creator = await db.creator.findUniqueOrThrow({
12 where: { id: b.creatorId },
13 });
14 
15 const res = await fetch(
16 `${API_BASE}/chats/mass-messages`,
17 {
18 method: "POST",
19 ...frontrow(creator),
20 body: JSON.stringify({
21 text: b.text,
22 includeLists: b.includeLists,
23 excludeLists: b.excludeLists,
24 }),
25 },
26 );
27 
28 if (res.status === 429) {
29 // Honor Retry-After and re-queue
30 const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
31 await queue.delay(b, retryAfter * 1000);
32 return;
33 }
34 
35 if (!res.ok) {
36 throw new Error(`broadcast ${res.status}: ${await res.text()}`);
37 }
38 
39 const { id, recipientCount } = await res.json();
40 await db.broadcast.update({
41 where: { id: b.id },
42 data: { sentAt: new Date(), apiId: id, recipientCount },
43 });
44}

See Sending mass messages for the full broadcast endpoint, smart-list IDs, and PPV mechanics.

Production checklist

  • Encrypt keys at rest. Use a managed KMS or libsodium sealed-box. Decrypt only in memory at the moment of use; never log the plaintext.
  • Cap concurrency. A worker that fans out across the whole roster can knock you offline. Eight concurrent creators is a sensible starting point.
  • Honor 429s. Read Retry-After and back off. A single chatty creator can block the rest of the roster if you retry tightly.
  • Audit log every action. Persist who-did-what (which staff member, which creator, what endpoint) so a creator can review activity at any time. Trust is the entire product.
  • Webhook-driven freshness. Long-poll syncs are wasteful and lag behind reality. Subscribe to subscriber.*, tip.received, and purchase.received for live counters.
  • Per-creator role boundaries. A staff member who only handles three creators should not see the others. Enforce row-level filters in your queries.