Better Changelog: Settings Page & Public Changelog Spec
Overview
Build the Settings page (multi-tab) and enhance the public changelog view.
1. Settings Page (/dashboard/settings)
Architecture
- Tab-based navigation (URL-driven:
/dashboard/settings/general,/dashboard/settings/team, etc.) - Server components for data fetching, client components for forms
- Role-based access: some tabs owner-only, some admin+
Tabs
1.1 General (/dashboard/settings or /dashboard/settings/general)
Access: Admin+
| Field | Type | Notes |
|---|---|---|
| Team Name | text input | Required, max 255 chars |
| Slug | text input | URL-safe, lowercase, auto-generated from name, editable |
| Logo | image upload | Store in R2/S3, display in public changelog header |
| Timezone | select | For scheduled publishing |
Actions:
- Save Changes button
- Preview link to public changelog
Validation:
- Slug: unique across all tenants, URL-safe characters only
- Logo: max 2MB, jpg/png/svg/webp
1.2 Branding (/dashboard/settings/branding)
Access: Admin+
| Field | Type | Notes |
|---|---|---|
| Primary Color | color picker | Hex, used for buttons/links |
| Secondary Color | color picker | Optional |
| Accent Color | color picker | Optional |
| Custom CSS | textarea | Advanced users, max 10KB |
| Show βPowered byβ | toggle | Free tier: forced on. Paid: can hide |
Preview: Live preview panel showing how public changelog looks with colors
1.3 Custom Domain (/dashboard/settings/domain)
Access: Owner only
| Field | Type | Notes |
|---|---|---|
| Custom Domain | text input | e.g., changelog.acme.com |
| Verification Status | badge | Pending / Verified / Failed |
Flow:
- User enters domain β we generate CNAME record instructions
- βVerify Domainβ button triggers DNS check
- Once verified, we configure SSL via Cloudflare/Vercel
Display:
- Current domain with status badge
- Instructions for CNAME setup
- βVerifyβ / βRemoveβ buttons
Notes:
- Defer actual implementation β just build the UI for now
- Backend domain routing will need middleware changes
1.4 Team (/dashboard/settings/team)
Access: Admin+ (but only owner can change roles or remove admins)
Current Members Table:
| Column | Notes |
|---|---|
| Avatar | From user profile |
| Name/Email | User info |
| Role | Badge: owner/admin/editor/viewer |
| Joined | Date |
| Actions | Change role (dropdown), Remove (button) |
Invite Form:
| Field | Type | Notes |
|---|---|---|
| text input | Required | |
| Role | select | admin/editor/viewer (not owner) |
| Send Invite | button | Sends email with invite link |
Pending Invites Section:
- List of pending invites with Resend/Cancel buttons
Role Permissions:
| Role | Entries | Settings | Team | Billing | Delete Org |
|---|---|---|---|---|---|
| Owner | β CRUD | β All | β All | β | β |
| Admin | β CRUD | β Most | β Invite/Remove editors | β | β |
| Editor | β CRUD | β | β | β | β |
| Viewer | β Read | β | β | β | β |
1.5 API Keys (/dashboard/settings/api)
Access: Admin+
Existing Keys Table:
| Column | Notes |
|---|---|
| Name | User-defined |
| Key Prefix | cl_live_xxxx... (masked) |
| Scopes | Badges |
| Created | Date |
| Last Used | Date or βNeverβ |
| Actions | Revoke button |
Create New Key Form:
| Field | Type | Notes |
|---|---|---|
| Name | text input | e.g., βCI/CD Pipelineβ |
| Scopes | multi-select | entries:read, entries:write, etc. |
| Expiration | select | Never / 30d / 90d / 1yr |
On Create:
- Show full key ONCE in modal (user must copy)
- Store only hash in DB
Security:
- Keys prefixed with
cl_live_orcl_test_ - Hash using SHA-256 before storage
- Rate limit API endpoints
1.6 Notifications (/dashboard/settings/notifications)
Access: Admin+
Email Settings:
| Field | Type | Notes |
|---|---|---|
| Send email on publish | toggle | Master switch |
| From Name | text input | e.g., βAcme Updatesβ |
| Reply-To | email input | Optional |
Webhook Settings:
| Field | Type | Notes |
|---|---|---|
| Webhook URL | text input | HTTPS required |
| Secret | auto-generated | For signature verification |
| Events | multi-select | entry.published, entry.updated, etc. |
| Active | toggle | Enable/disable |
Webhook List:
- Table of configured webhooks with Edit/Delete/Test buttons
1.7 Danger Zone (/dashboard/settings/danger)
Access: Owner only
Actions:
| Action | Confirmation | Notes |
|---|---|---|
| Export Data | None | Download all entries as JSON |
| Transfer Ownership | Email + Password | Transfer to another team member |
| Delete Organization | Type org name + password | Permanent, cascades all data |
UI: Red-bordered section, clear warnings
2. Public Changelog Page (/c/[tenant])
Current State
Basic list of entries with minimal styling.
Enhancements
2.1 Header
- Tenant logo (if uploaded)
- Tenant name
- Optional tagline/description
- RSS feed button
- Subscribe button (opens email modal)
2.2 Filter Bar
- By update type (tabs or pills)
- By date range (optional)
- Search (optional, defer)
2.3 Entry Cards
- Date prominently displayed
- Type badge with color
- Title with emoji
- Summary text
- βRead moreβ link to full entry
2.4 Subscribe Modal
- Email input
- Frequency preference (realtime/daily/weekly)
- Checkbox for specific audiences/platforms (optional)
- Double opt-in flow
2.5 Custom Domain Support
- When accessed via custom domain, style should apply
- βPowered by Changelogβ footer (hideable for paid)
2.6 Branding
- Apply tenantβs brandColors to:
- Primary buttons/links
- Header background (subtle)
- Badge colors
- Apply customCss if set
3. Database Migrations Needed
Already exists:
tenants.brandColors(jsonb)tenants.customDomain,customDomainVerifiedtenants.logoUrltenants.settings(jsonb)tenantMembers.roleapiKeystablewebhookstablesubscriberstable
Need to add:
team_invitationstable (if not exists)
CREATE TABLE IF NOT EXISTS team_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
token VARCHAR(255) NOT NULL UNIQUE,
invited_by UUID REFERENCES users(id),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);4. Implementation Order
Phase 1: Core Settings (Today)
- Settings layout with tab navigation
- General tab (name, slug, logo upload via URL for now)
- Team tab (list members, change roles, remove)
- Danger Zone (delete org with confirmation)
Phase 2: Branding & Public Changelog
- Branding tab (colors, custom CSS)
- Apply branding to
/c/[tenant]page - Add filter bar to public changelog
- Subscribe modal (basic)
Phase 3: Advanced Features (Later)
- API Keys tab (generate, revoke)
- Custom Domain tab (UI only, defer routing)
- Notifications tab (webhooks, email settings)
- Team invitations (email flow)
5. Risk Mitigation
| Risk | Mitigation |
|---|---|
| Slug change breaks links | Warn user, suggest redirect setup |
| Logo upload abuse | Size limit (2MB), sanitize filenames, consider virus scan |
| Custom CSS XSS | Sanitize CSS, no url(), no expression(), no javascript: |
| API key leakage | Show full key only once, store hash only |
| Accidental org deletion | Require typing org name + password |
| Domain verification spoofing | Use unique verification tokens per tenant |
6. Files to Create
apps/web/src/app/dashboard/settings/
βββ layout.tsx # Tab navigation
βββ page.tsx # Redirects to /general
βββ general/
β βββ page.tsx # General settings form
βββ branding/
β βββ page.tsx # Branding settings
βββ domain/
β βββ page.tsx # Custom domain
βββ team/
β βββ page.tsx # Team management
βββ api/
β βββ page.tsx # API keys
βββ notifications/
β βββ page.tsx # Email & webhooks
βββ danger/
βββ page.tsx # Danger zone
apps/web/src/components/settings/
βββ general-form.tsx
βββ branding-form.tsx
βββ team-table.tsx
βββ invite-form.tsx
βββ api-keys-table.tsx
βββ create-api-key-form.tsx
βββ webhooks-table.tsx
βββ delete-org-dialog.tsx
βββ color-picker.tsx
apps/web/src/lib/actions/
βββ settings.ts # Update tenant settings
βββ team.ts # Manage members, invites
βββ api-keys.ts # CRUD for API keys
7. Approval
Spec reviewed for:
- Schema compatibility β all fields exist or have migration plan
- Role-based access β documented per tab
- Security risks β identified and mitigated
- Implementation order β phased, MVP-first
- UX β tab-based, familiar pattern
Ready to implement Phase 1.