DeployLog
Keeping a changelog current is the chore every dev team drops first.
DeployLog makes it automatic: publish entries to a hosted page and an embeddable widget, or let a published npm CLI turn git commits into release notes with an LLM. Built solo, end to
end.
deploylog.dev: the landing page also embeds DeployLog's own live widget
One Platform, Four Publishing Surfaces
A changelog is only useful where your users already are, so a single entry written once in the dashboard fans out to four surfaces: a hosted, SSR changelog page with per-entry canonical URLs and RSS/JSON feeds; an embeddable widget (vanilla TypeScript in a Shadow DOM, bundled with esbuild, no framework dependency) dropped in with one script tag; a published npm CLI; and a GitHub Action that publishes straight from a Release.
The two write-paths (dashboard and CLI/Action) hit the same API but never share an auth method. See below.
<!-- One script tag embeds the live changelog widget -->
<script
src="https://cdn.deploylog.dev/widget.js"
data-project="your-project-slug"
defer
></script> Two Auth Models That Never Mix
The dashboard authenticates with a Supabase cookie session and runs
every query through the anon key, so Postgres Row-Level Security does the access control. The CLI and integrations authenticate with
a dk_-prefixed API key (stored only as a SHA-256 hash) and run through the
service-role client, which bypasses RLS, so those endpoints must
filter by org_id explicitly. The rule that keeps it safe: a given route handler uses
exactly one of these, never both. CLI-only routes live under their own
/api/cli/* namespace.
Capability-Based Plan Gating + Stripe Billing
Every paid feature reads through one primitive (can(org, capability)) backed by a truth table keyed on the org's plan. Adding a new
gated feature is one row in the table plus one call at the gate site,
rather than scattering plan === "pro" checks across the codebase. Stripe is the source of truth for plan
state: Checkout and the Customer Portal drive everything, and a
webhook reconciles the org's plan on every subscription change, with a
stripe_events table providing idempotency.
// src/lib/plan.ts: one source of truth for every feature gate
export function can(
org: { plan: Plan } | null,
capability: Capability,
): boolean {
return CAPABILITIES[org?.plan ?? "free"].includes(capability);
}
// Gate site: projects API
if (!can(org, "unlimited_projects") && projectCount >= FREE_PROJECT_LIMIT) {
return error("PLAN_LIMIT", "Upgrade to add more projects", 403);
} AI Release Notes, Shared by CLI and Dashboard
Both the dashboard editor and the CLI can turn raw commit messages into a clean, user-facing entry (title, type, Markdown body) with Claude Haiku. The two surfaces are thin auth shells over one shared service: the prompt, the JSON-validated output, the rate limit, and the free-tier monthly cap all live in a single module, so the two entry points can't drift apart. From the terminal it's one flag:
# Generate release notes from git, rewritten by an LLM, and publish
deploylog push --from-git --ai-summarize Stack
Frontend / API: Next.js 16 (App Router, RSC), React 19, TypeScript (strict), Tailwind v4
Database / auth: Supabase (PostgreSQL + RLS, GitHub OAuth)
Payments: Stripe (Checkout, Portal, webhooks)
Email: Resend + React Email templates
AI: Anthropic Claude (Haiku) for release-note summarization
Editor / widget: CodeMirror 6 · vanilla-TS Shadow-DOM widget (esbuild)
Infra: Vercel · Cloudflare Pages (widget CDN) · Upstash · Sentry
Distribution: npm CLI · GitHub Action on the Marketplace
Screenshots
deploylog.dev: hero, CTAs, and the self-hosted live widget
Retrospective
- // One capability table beats scattered plan checks.
Routing every gate through
can()meant adding the Pro tier late in development touched one file, not twenty. - // Sharing one service across CLI and dashboard prevents drift. The AI-summarize logic lives once; the two surfaces are just auth + transport. The same shape would have saved time if applied to more dual-surface endpoints from the start.
- // The widget being framework-free was the right call. A Shadow-DOM vanilla-TS bundle drops into any site without pulling in React or clashing with host styles, the constraint that made it small also made it universal.