The Input atom defaults to h-9 (36px) while the size="sm" filter
Buttons in the same row are h-8 (32px). On /workflows the search bar
is alone in the row so the mismatch isn't visible. On /templates and
/campaigns the type/status filter buttons sit next to the search bar
and the row stretches to the Input's 36px, visibly offsetting the
buttons. Shrink the search Input to h-8 text-xs on all three pages
and add sm:items-center to the filter rows so the controls vertically
center.
Currently the only entry point from the /contacts list to a contact's
detail page is the small Edit icon-button in the actions column. Make
the email itself a Link to /contacts/:id so the obvious affordance
("click the thing that identifies the row") works too. Applied to both
the desktop table cell and the mobile card variant.
PR review (#371): scope the self-host baseline to envs that make sense
without reselling Plunk.
docker-compose.yml: pass through only DISABLE_SIGNUPS (private/single-
admin self-host) and EMAIL_RATE_LIMIT_PER_SECOND (works around the
silent 14/sec fallback when ses:GetSendQuota is denied). Drop the
VERIFY_EMAIL_ON_SIGNUP and OPENROUTER_API_KEY passthroughs — signup
hygiene and phishing detection are reselling concerns.
.env.self-host.example: drop the entire Stripe Billing block (billing
only matters when reselling), drop the VERIFY_EMAIL_ON_SIGNUP doc
block for the same reason, and add an EMAIL_RATE_LIMIT_PER_SECOND
section explaining the silent-14/sec fallback so operators know why
they'd set it.
Replace per-route apiKey/jwt branching with auth.projectId from middleware,
matching the contacts controller pattern. Preserve JWT admin gating on
POST/DELETE; API keys are project-scoped by design and skip the role check.
Cross-project domain access by ID now returns 404 instead of 403 to avoid
leaking existence.
Switch /domains controller from `isAuthenticated` (cookie-only) to
`requireAuth` (cookie OR API key), matching the pattern used by other
project-scoped API endpoints (/v1/send, /contacts, etc.).
API keys are project-scoped credentials with full access; for write
operations the projectId in the request must equal the API key's
projectId. JWT (dashboard) auth retains role-based checks
(requireAdminAccess for POST/DELETE).
Also: improve UX when a domain is already linked to the same project
by returning a clear error instead of the generic "linked to another
project" message.
Refactor DomainService.checkDomainOwnership to make `userId` optional
(needed for API key path) while preserving its existing return shape
and adding `projectId` to the result.