Projects
Launching · solo, full-stack Next.js · Supabase · Stripe

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 landing page: Ship the update. Skip the busywork.

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 landing page hero and live widget

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.