Everything the bmbb-portal session needs to render live tiles from the six SaaS apps. The provider side (each app's /api/v1) is built & deployed — this is the consumer side. Paste-ready; restyle to match the portal.
While signed into each app, POST /api/keys (Localey: use its existing BOO/Dashee key). Each returns the plaintext once. Put them in the bmbb-portal env:
# bmbb-portal .env
REPLYEE_API_KEY=rpl_live_...
RANKEE_API_KEY=rk_live_...
BLOGGY_API_KEY=blg_live_...
LOCALEY_API_KEY=lc_... # Localey's existing key
POSTTEE_API_KEY=pt_live_...
ASSISTEE_API_KEY=ast_live_...
Mint via curl (example — Bloggy), copy the key from the response:
curl -X POST https://bloggy.online/api/keys \
-H "Content-Type: application/json" \
-H "Cookie: <your logged-in session cookie>" \
-d '{"label":"BMBB dashboard"}'
| App | Endpoint | Auth header | Key returns (fields) |
|---|---|---|---|
| Rankee | GET /api/v1/summary?domain= | Authorization: Bearer | websites, keywords_tracked, avg_rank, latest_audit |
| Bloggy | GET /api/v1/summary | Authorization: Bearer | posts, published, drafts, scheduled, latest_posts[] |
| Localey | GET /api/v1/summary | X-API-Key | avg_rating, review_count, new_reviews_30d, citations_live/total |
| Posttee | GET /api/v1/summary | Authorization: Bearer | scheduled, published_this_month, connected_accounts |
| Assistee | GET /api/v1/devices | Authorization: Bearer | total, online, offline |
| Replyee | GET /api/v1/me | Authorization: Bearer | bots[] — leads already flow to /admin/leads via the webhook |
/api/v1/me is available if you want a small "N bots active" tile.One config map, one function. Server-side only (keys never reach the browser). 5-min cache.
// bmbb-portal/src/lib/boomApi.ts
type AppKey = 'replyee' | 'rankee' | 'bloggy' | 'localey' | 'posttee' | 'assistee'
const APPS: Record<AppKey, { base: string; path: string; auth: 'bearer' | 'x-api-key'; env: string }> = {
replyee: { base: 'https://replyee.online', path: '/api/v1/me', auth: 'bearer', env: 'REPLYEE_API_KEY' },
rankee: { base: 'https://rankee.online', path: '/api/v1/summary', auth: 'bearer', env: 'RANKEE_API_KEY' },
bloggy: { base: 'https://bloggy.online', path: '/api/v1/summary', auth: 'bearer', env: 'BLOGGY_API_KEY' },
localey: { base: 'https://localey.online', path: '/api/v1/summary', auth: 'x-api-key', env: 'LOCALEY_API_KEY' },
posttee: { base: 'https://posttee.online', path: '/api/v1/summary', auth: 'bearer', env: 'POSTTEE_API_KEY' },
assistee: { base: 'https://assistee.online', path: '/api/v1/devices', auth: 'bearer', env: 'ASSISTEE_API_KEY' },
}
export async function fetchAppSummary(app: AppKey, query = ''): Promise<Record<string, unknown>> {
const cfg = APPS[app]
const key = process.env[cfg.env]
if (!key) return { ok: false, error: 'missing key' }
const headers: Record<string, string> =
cfg.auth === 'bearer' ? { Authorization: `Bearer ${key}` } : { 'X-API-Key': key }
try {
const res = await fetch(cfg.base + cfg.path + query, { headers, next: { revalidate: 300 } })
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
return await res.json()
} catch {
return { ok: false, error: 'unreachable' }
}
}
Generic tile: title, a big stat, a sub-line. Server component — it fetches, so no client key exposure. Restyle to the portal's look.
// bmbb-portal/src/components/SummaryCard.tsx (server component)
export function StatTile({ label, value, sub }: { label: string; value: string | number; sub?: string }) {
return (
<div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 12, padding: 18 }}>
<div style={{ fontSize: 12, color: '#6b7280', fontWeight: 600 }}>{label}</div>
<div style={{ fontSize: 28, fontWeight: 800, margin: '4px 0' }}>{value}</div>
{sub && <div style={{ fontSize: 12, color: '#9ca3af' }}>{sub}</div>}
</div>
)
}
import { fetchAppSummary } from '@/lib/boomApi'
import { StatTile } from '@/components/SummaryCard'
export default async function DashboardCards() {
const [rankee, localey, posttee, assistee] = await Promise.all([
fetchAppSummary('rankee', '?domain=bigmikesbailbonds.com'),
fetchAppSummary('localey'),
fetchAppSummary('posttee'),
fetchAppSummary('assistee'),
])
const n = (v: unknown) => (typeof v === 'number' ? v : v == null ? '—' : String(v))
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(180px,1fr))', gap: 14 }}>
<StatTile label="Avg Google Rank" value={n(rankee.avg_rank)} sub={`${n(rankee.keywords_tracked)} keywords`} />
<StatTile label="Review Rating" value={n(localey.avg_rating)} sub={`${n(localey.review_count)} reviews · ${n(localey.new_reviews_30d)} new`} />
<StatTile label="Scheduled Posts" value={n(posttee.scheduled)} sub={`${n(posttee.published_this_month)} published this mo`} />
<StatTile label="Devices Online" value={`${n(assistee.online)}/${n(assistee.total)}`} sub="remote support" />
</div>
)
}
0/null. If a tile reads blank on first load, tell Claude which app and it'll fix that one column (known suspects: Bloggy post status values, Posttee pt_posts.user_id, Localey lc_reviews.rating).Wire six live SaaS cards into this bmbb-portal dashboard. Use the fetch
helper + StatTile from the Boom Labs handoff (boomApi.ts config map: Rankee/
Bloggy/Posttee/Assistee via Authorization: Bearer, Localey via X-API-Key,
endpoints /api/v1/summary except Assistee /api/v1/devices). Keys are in env
(REPLYEE/RANKEE/BLOGGY/LOCALEY/POSTTEE/ASSISTEE_API_KEY). Render tiles matching
the portal's existing card style; server-side fetch only. Replyee's leads
already arrive via webhook on /admin/leads, so no Replyee card needed.
/api/v1) is live — see API Access Hub.