All notable changes to ByteSend are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Ban enforcement across all layers — the existing
isBannedflag onUseris now fully enforced: login is blocked at thesignIncallback with a redirect to/login?error=banned; the JWT token embedsisBannedso middleware can gate all protected routes; everyauthedProceduretRPC call re-validates against the database; and the public REST API blocks requests where the team's admin is banned - Banned user page (
/banned) — dedicated page shown to banned users with account suspension message and a "Join our Discord" link for false-ban appeals - AGENTS.md — documentation file at the repo root describing project architecture, dev commands, auth layers, conventions, and testing notes for AI coding agents
- Bounce/complaint rate enforcement — 7-day rolling hard-bounce rate > 2% or complaint rate > 0.1% (Google/Yahoo thresholds) auto-blocks a team's sending with a Discord alert; warns at 1.5% / 0.08%; minimum volume of 100 emails required before enforcement kicks in; uses
LimitReason.BOUNCE_RATE_EXCEEDED/LimitReason.COMPLAINT_RATE_EXCEEDED - RFC 8058 one-click unsubscribe compliance —
List-Unsubscribeheader now lists the POST-capable one-click endpoint first (required by Gmail/Yahoo), with the click-based/unsubscribeURL as a fallback for Outlook;List-Unsubscribe-Post: List-Unsubscribe=One-Clickheader added automatically for marketing emails; the one-click route now also accepts GET requests with a 302 redirect to the user-facing unsubscribe page for click-based mail clients unsubOneClickUrlthreading — one-click URL generated per email inexecuteEmail()and passed throughsendRawEmail()→buildHeaders()
- Team-scoped bounce/complaint notifications — SES hook parser now routes bounce and complaint events through
NotificationProviderService.broadcastNotification()to the team's own configured providers instead of the platform-level admin Discord; notification content omits raw recipient addresses (count only) ADMIN_DISCORD_WEBHOOK_URLenv var — optional platform-level observer webhook; when set,broadcastNotification()fans out a copy of every team event to this URL after dispatching to team providers; teams cannot register this URL as their own provider- Admin webhook collision guard —
validateConfig()inNotificationProviderServicerejects any Discord or Slack webhook URL that matchesADMIN_DISCORD_WEBHOOK_URL
- Domain force-verify — admins can set a domain's status to
SUCCESSdirectly from the Domains admin page (useful for stuck verifications); shield icon button appears on non-verified domains - Domain delete — admins can permanently delete any domain from the Domains admin page
- Team delete — permanent team deletion with browser confirmation dialog from the team detail view's new Danger Zone section
- Extra domain/member slots — team settings form now exposes
extraDomainSlotsandextraMemberSlotsfields so admins can grant bonus capacity without changing a team's plan - Complimentary plan assignment for all admins —
adminAssignPlanno longer requiresisEnvAdmin; any admin can now assign plans complimentarily (no charge) or generate Stripe checkout links; plan assignment UI visible to allisAdminusers (previously onlyisEnvAdmin) - Dual plan assignment buttons — teams page now shows both "Assign complimentary" (immediate, no charge) and "Checkout link" (Stripe) buttons; checkout link button disabled for FREE plan
updateTeamSettings— removedisEnvAdminguard for plan changes; all admins can update team settings including plan; addedextraDomainSlotsandextraMemberSlotsto the mutation input and DB updateteamAdminSelection— now includesisActive,extraDomainSlots, andextraMemberSlotsso the admin UI reflects the full team stateauthedProcedure— now performs a fresh database ban check on every call rather than relying solely on the JWT token
UpgradeModaltype error — added missingCONTACTS,BOUNCE_RATE_EXCEEDED, andCOMPLAINT_RATE_EXCEEDEDentries to theRecord<LimitReason, string>messages map
- Backup email password-protected alternative login — users can add password-protected backup emails to their account for login without email OTP; backup emails must be verified independently via 6-character code
- Two-sided email verification — changing primary email now requires verifying both old email access (confirm you can access current email) AND new email ownership (confirm you own the new email); sequential step-by-step verification with clear UI indicators
- Recovery code bypass for email verification — if user loses access to their primary email, they can use a recovery code to skip old email verification and proceed directly to new email verification
- Backup email schema — added
BackupEmailmodel with email, passwordHash, emailVerified timestamp, and indexes for efficient login lookups - Credentials provider for NextAuth — added Credentials provider configuration allowing backup email + password authentication through standard NextAuth flow
- Account settings page — new
/settings/accountconsolidating:- Email address change with two-sided verification flow
- Two-factor authentication setup/management
- Recovery codes display and regeneration
- Backup email management (add, verify, delete with guards)
- Broadcasts compose page refactor — completely redesigned broadcasts compose UI to match campaigns aesthetic:
- Accordion-style settings strip (Subject visible, From/Reply-To/Recipients collapsible)
- Full-width email canvas for better content editing
- Variables strip showing available variables and recipient count
- Cleaner, more professional layout with proper spacing
- Login page backup email option — toggleable backup email login form separate from primary auth methods; only appears when user opts in via "Have a backup email password?" link
- 2FA verification page styling — professional card layout with gradient backgrounds, improved form labels (context-aware "Authenticator Code" vs "Recovery Code"), better help text and mobile responsiveness
- Backup email schema (
20260603195140_add_backup_emails_and_two_sided_verification):- Added
BackupEmailmodel with unique email per user, passwordHash, emailVerified nullable timestamp - Added
PendingBackupEmailVerificationmodel with 6-char code, expiry, unique constraint on [userId, email] - Enhanced
PendingEmailChangewith codeOld (nullable), verifiedOld, verifiedNew flags for two-sided verification
- Added
- bcryptjs password hashing — backup email passwords hashed with bcryptjs (salt rounds 10) on client-side before transmission
- Edge Runtime 2FA utilities — new
edge-2fa-utils.tsusing Web Crypto API for middleware HMAC validation (no Node.js crypto dependency) - 2FA cookie validation — 12-hour HMAC-signed httpOnly cookie for 2FA sessions with middleware validation for protected routes
- Upgraded Next.js to latest — updated to the latest stable version for improved performance, security, and feature support
- NextAuth configuration — added Credentials provider for backup email authentication alongside existing OAuth providers (GitHub, Google, Discord) and Email OTP
- Login page provider filtering — Credentials provider now explicitly excluded from OAuth buttons loop to prevent rendering as a regular provider button
- Broadcasts vs Campaigns routing — campaign cards now differentiate routing based on intent; broadcasts route to
/broadcasts/[id]while campaigns route to/campaigns/[id] - Settings navigation restructuring — reorganized settings tabs with top-level Account and Team sections
- Email change mutations —
requestEmailChangenow sends both old and new verification codes in parallel; introduces verifiedOld/verifiedNew state tracking - Mailer service — added optional
subjectparameter tosendEmailChangeVerificationEmail()for flexibility in notification messages - Form validation consistency — all form validators aligned with mutation input types for seamless error handling
- Credentials provider filtering — removed Credentials provider from OAuth buttons array to prevent it from rendering as a sign-in option button
- Build and deployment — resolved AWS SDK package corruption from earlier disk space issues with clean reinstall
- Email address change with verification — users can now change their account email via a re-verification flow: request new email → verification code sent → confirm with 6-character code (15-min expiry) → email updated and verified
- Account-level two-factor authentication (2FA) — TOTP-based 2FA using authenticator apps; includes:
- Setup wizard with QR code and manual secret fallback
- Recovery codes (10 × 10-character hex codes per setup) for account recovery if authenticator is lost
- Timing-safe verification with SHA-256 hashing for recovery codes
- Single-use recovery code tracking (mark used, count remaining)
- Regenerate recovery codes option (requires current TOTP verification)
- Server-enforced 2FA — 2FA verification enforced at the middleware level via HMAC-signed httpOnly cookies (12-hour expiry); users with 2FA enabled are redirected to
/auth/2fa-verifyif cookie is missing/invalid - OAuth account linking safety — users with OAuth-linked accounts (GitHub, Google, Discord) cannot change email to prevent account takeover via dangerous email account linking
- Direct broadcast recipient support — campaigns with
intent: "BROADCAST"can now accept direct recipient email lists viarecipientEmailsfield; recipients need not be in a contact book - Broadcast batch processing —
CampaignBatchService.workernow handles direct broadcasts by:- Iterating recipient email list
- Checking suppression/bounce status
- Creating Email and CampaignEmail records
- Queuing via EmailQueueService (no contact book required)
- Deduplicating via CampaignEmail lookup
- Dedicated broadcast compose UI — new
/broadcasts/[broadcastId]/composepage with:- Toggle between contact book and direct recipient modes
- Direct recipient textarea with email parsing and validation
- Simplified UX (no campaign automation, direct send or schedule)
- Auto-save on field/content changes
- Send now vs. schedule workflows
- Account section — new
/settings/accountpage consolidating:- Email address change (with OAuth provider notice for linked accounts)
- Two-factor authentication setup/disable
- Recovery codes management (display and regenerate)
- Team section — renamed from "General";
/settings/teamnow contains:- Team image upload/management
- Team name editing
- Danger zone (delete team)
- Docker manifest publishing robustness —
create_and_publish_manifestjob now waits for platform-specific images (amd64, arm64) to be available before creating multi-platform manifests; prevents "image not found" errors on manifest creation - Edge Runtime compatible 2FA utilities — new
edge-2fa-utils.tsusing Web Crypto API (no Node.js crypto) for middleware HMAC validation in Edge Runtime - Dynamic rendering for search params —
/auth/2fa-verifyusesSuspenseboundary pattern for safeuseSearchParams()usage in Next.js 15
- Profile query enrichment —
user.getProfilenow includes linked OAuth accounts (type,provider) to allow UI checks for password/email change eligibility - Email change mutations —
requestEmailChangevalidates OAuth account presence and blocks email changes for OAuth-linked users
- Campaign update accepts recipient emails —
updateCampaignmutation now accepts optionalrecipientEmails: string[]input for direct broadcast editing
- Campaign card routing logic —
CampaignCardnow differentiates broadcast vs. campaign routing:- Broadcasts:
/broadcasts/[id]/compose(DRAFT/SCHEDULED) or/broadcasts/[id](SENT/RUNNING) - Campaigns:
/campaigns/[id]/edit(DRAFT/SCHEDULED) or/campaigns/[id](SENT/RUNNING)
- Broadcasts:
- Broadcasts landing page —
/broadcastsnow usesCreateBroadcastdialog (separate fromCreateCampaign)
- Settings layout tabs — updated nav buttons:
- Changed "General" → "Team"
- Added "Account" tab
- Root
/settingsredirects to/settings/team
- Next.js 15 prerender error for dynamic routes —
/auth/2fa-verifynow uses server component + Suspense boundary to avoid prerender failure when usinguseSearchParams() - Edge Runtime crypto import errors — middleware no longer imports Node.js
cryptomodule; uses Web Crypto API for HMAC validation instead
- Manifest creation timing issue — workflow no longer fails immediately when platform images haven't finished pushing;
wait_for_remote_image()now called for both amd64 and arm64 images before manifest creation (18 retry attempts, 10-second intervals = 180-second timeout)
- Migration: Email change re-verification (
20260603032501_email_change_reverification_and_account_2fa)- Added
emailChangeToken,emailChangeTokenExpires,twoFactorEnabled,twoFactorSecret,twoFactorTempSecrettoUsermodel
- Added
- Migration: 2FA recovery codes (
20260603072201_2factor_recovery_codes)- Added
TwoFactorRecoveryCodemodel with userId FK, codeHash, used, usedAt, createdAt - Added index on
userId, usedfor efficient unused code lookup
- Added
- Migration: Direct broadcast recipients (
20260603072459_add_recipient_emails_to_campaign)- Added
recipientEmails String[]toCampaignmodel for direct broadcast recipient storage
- Added
0.2.6 - 2026-05-10
- Modular homepage architecture — split the landing page into reusable section components (
Hero,TrustStrip,Features,Comparison,PricingSection,CallToAction,DevSection) for clearer ownership and easier iteration - Single pricing calculator flow — promoted the calculator-led pricing experience and removed older card-based pricing composition in favor of a unified pricing section
- Broadcasts section — introduced a dedicated
/broadcastsroute and page hierarchy (/broadcasts,/broadcasts/[broadcastId],/broadcasts/[broadcastId]/edit) for one-off email blasts, separating them from the Campaigns flow which is now focused on recurring/automated sequences
- Unified logs dashboard — added a first-class
/logsdashboard page that merges email events, webhook calls, and notification delivery logs into one searchable audit trail with source and status filtering
- Settings API Keys page — added first-class
/settings/api-keysroute - Settings SMTP page — added first-class
/settings/smtproute
- Slider-based plan selector —
BillingPlanSelectorcomponent replaces static plan cards at/settings/billingwith interactive marketing and transactional email limit sliders (1,000 – 3,000,000 each); calculated contract total updates in real time as limits are adjusted - Custom Stripe checkout sessions —
createCustomCheckoutSessiontRPC mutation creates a Stripe session withprice_data(unit_amount = exact contract price in CAD) rather than a fixed price ID, locking teams into the exact limit/price combination they selected - Custom plan query —
getCustomPlanContracttRPC query lets the billing page andPlanDetailscomponent surface the active custom contract (limits + monthly price) for teams already on a custom plan - Custom plan DB fields — added four optional fields to the
Teammodel (customPlanEnabled,customMarketingEmailLimit,customTransactionalEmailLimit,customMonthlyPriceCents); populated at checkout completion via the Stripe webhook
- Campaign intent field — campaigns now store an
intentcolumn (migration20260510120000_add_campaign_intent) to distinguish campaign purpose at creation time;create-campaigndialog captures intent upfront
- Go SDK initial package — introduced
packages/go-sdkwith typed client surfaces for emails, contacts, contact books, campaigns, domains, and analytics
- SMTP auth API reference page — added
apps/docs/api-reference/smtp/auth.mdx - Self-hosting Docker doc relocation — promoted Docker setup docs to
apps/docs/self-hosting/docker.mdxand aligned navigation
- CodeRabbit support — added
.github/workflows/coderabbit.ymland root.coderabbit.yamlto enable automated PR review summaries and inline feedback on pull requests
- Homepage composition refresh — replaced older section files (
FeatureCard*,PricingTiers,CodeExample) with the new component set and updated page assembly - Icon system modernization — replaced hand-authored inline SVG usage in key marketing and app screens with icon components for consistency and maintainability
- Developer section improvements — expanded dev-focused section behavior and language-toggle handling in
CodeLangToggleandDevSection
- Developer settings consolidation — aligned developer tooling pages with the canonical Settings area while preserving compatibility flows from legacy dev-settings routes
- Pricing/plan constant updates — refreshed shared Stripe plan/product definitions and app-side plan/payment constants to keep UI, checkout, and limits in sync
- Billing page rebuilt around custom plan selector —
/settings/billingnow surfaces the slider-basedBillingPlanSelectoras the primary upgrade path;PlanDetailsshows active custom contract details (limits and monthly price) when a custom plan is active - Custom plan limit enforcement —
LimitServicenow readscustomMarketingEmailLimitandcustomTransactionalEmailLimitfrom theTeamrecord and enforces per-type monthly caps for custom plan teams, bypassing the standard plan-tier daily limits; the daily usage job skips metered overage billing for custom plan teams to avoid double-charging - Admin plan assignment clears custom contract —
adminAssignPlanwithmethod: "complimentary"now explicitly clears all custom plan fields (customPlanEnabled, all limit columns) so stale slider contracts do not persist after a manual admin override - Free-plan marketing access switched to limit-based enforcement — Contacts, Broadcasts, and Campaigns are no longer hard-locked in the dashboard for Free teams; access is now controlled by enforceable plan limits (including campaign count and monthly sending caps)
- Free plan marketing baseline updated — FREE plan now includes limited marketing capability (
marketingEmailsIncluded: true) with a capped campaign allowance (campaignsLimit: 3) instead of full marketing exclusion - Stripe seed updates — adjusted
stripe-seed.tsfor current product/price setup behavior
- Analytics detail expansion — dashboard analytics now includes additional breakdown cards (delivery rate, bounce rate, complaint rate, and total volume) for clearer at-a-glance performance tracking
- Paid-only advanced analytics insights — added a paid-tier insights section for open rate, click rate, click-to-open rate, and average daily volume with an upgrade CTA for free teams
- Full usage limit breakdown — the
/settings/usagepage now shows a complete limit audit for all tracked resources, including monthly and daily email usage, marketing vs transactional email usage, domains, contact books, contacts, campaigns, team members, and webhooks, so teams can see both current usage and allowed limits in one place
- OpenAPI refresh — regenerated and updated API reference spec and intro content
- Docs navigation/content refresh — updated docs navigation and onboarding pages (including Go and self-hosting paths) to match current product structure
- Settings route reliability — removed dead-end developer settings destinations by wiring pages into canonical Settings routes and maintaining redirect compatibility
- Admin bypass email normalization —
LimitService.isAdminOrFounderTeamnow normalises the environment admin email withtrim()+toLowerCase()and uses a case-insensitive Prisma query, preventing mismatches caused by casing differences in env config - Custom plan Stripe webhook sync —
syncStripeDatanow readscustomPlanEnabled,customMarketingEmailLimit,customTransactionalEmailLimit, andcustomMonthlyPriceCentsfrom subscription metadata and persists them back to theTeamrecord so contract limits survive server restarts and are always in sync with Stripe
- Cross-page icon sizing/alignment — normalized icon rendering across footer, auth, error, not-found, and dashboard surfaces after component migration
- UpgradeModal scroll — fixed scroll behavior in the upgrade and notification modals so long plan/feature lists are fully accessible without clipping
0.2.5 - 2026-05-09
- GitHub OAuth support — added GitHub as a sign-in provider alongside Discord (
GITHUB_ID/GITHUB_SECRET), enabling GitHub auth for cloud and self-hosted deployments
- Multi-provider notifications — teams can now configure external alerting providers for operational events (email, campaign, domain, and error notifications)
- Supported providers — Discord, Slack, Microsoft Teams, Telegram, and Custom Webhook are now supported with provider-specific configuration
- Notification provider schema — added
NotificationProvidermodel to store provider type, config, status, and team linkage - Notification log schema — added
NotificationLogmodel to store per-dispatch delivery outcomes and failure details - Notification provider API — added
notificationProvidertRPC router withlist,getById,create,update,delete,test,getLogs, andgetStats - Notification dispatcher services — added provider dispatch and event emission services (
notification-provider-service.tsandnotification-emitter.ts) - Notifications settings page — added
/settings/notificationsUI for provider management, testing, logs, and usage guidance - Notification integration reference — added
.references/notification-integration.md
- Admin plan assignment flow — added
adminAssignPlanto let cloud admins assign plans to teams either as complimentary grants or Stripe checkout-link driven assignments - Admin team billing controls — admin team settings now supports
dailyEmailLimit = -1for unlimited daily sending
- Perks derived from shared plan constants —
apps/web/src/lib/constants/payments.tsnow generates plan perks from@bytesend/libPLANS rather than static duplicated data - Billing plan cards from shared plans —
/settings/billingplan options now derive from the shared PLANS map to avoid UI/config drift
- Issue summary workflow — added
.github/workflows/issue-summary.ymlto automatically summarize newly opened issues - Stale cleanup workflow — added
.github/workflows/stale-cleanup.ymlto clean up inactive issues and pull requests - CodeQL workflow — introduced
.github/workflows/codeql.ymland enableddevelopbranch triggers - JavaScript SDK release workflow — added
.github/workflows/npm-release.ymlto build and publish thebytesend-jspackage frompackages/sdkchanges onmain(plus manual dispatch) - Python SDK release workflow — added
.github/workflows/pypi-release.ymlto build and publish thebytesend-pythonpackage frompackages/python-sdkon pushes tomainand manual dispatch
- Repository security policy — added
.github/SECURITY.mdwith supported versions, private reporting process, and response expectations - Code of Conduct — added
.github/CODE_OF_CONDUCT.md(Contributor Covenant v2.1) - Contributing guide — added
.github/CONTRIBUTING.mdwith development workflow, PR expectations, and testing checklist - Support guide — added
.github/SUPPORT.mdwith support channels and security-report routing
- PR template — added
.github/PULL_REQUEST_TEMPLATE.mdto standardize change summaries, testing notes, and release-impact checks - Issue template config — added
.github/ISSUE_TEMPLATE/config.ymlwith contact links and blank-issue controls - New issue forms — added
.github/ISSUE_TEMPLATE/feature.ymland.github/ISSUE_TEMPLATE/docs.yml
- BASIC plan updated — aligned plan limits/pricing model by setting BASIC to CA$20/mo with 100,000 monthly emails, 30 members, and 12 domains
- LIFETIME plan updated — aligned lifetime limits to current plan progression at CA$199 one-time with 500,000 monthly emails, 100 members, and 30 domains
- Settings navigation restructured — removed Team inner General/Members subtabs and promoted them to top-level Settings navigation
- General tab behavior —
/settingsnow serves as the General overview (team profile and core team settings) - Members tab split-out — members management moved to dedicated
/settings/memberstab alongside billing/usage-related settings - Usage resource breakdowns — usage view now includes explicit domain, webhook, and member usage breakdowns with limit context
- SMTP server vendored into monorepo —
apps/smtp-serveris now tracked directly in this repository (no gitlink/submodule-style entry), simplifying versioning and release consistency - Authentication compatibility fallback — SMTP auth now supports the API-driven custom team username flow while retaining a legacy fallback username candidate for older client configurations
- SMTP docs refreshed for monorepo paths — clone/build/deployment documentation now references
NodeByteLTD/ByteSendandapps/smtp-serverpaths throughout - SMTP quickstart clarified — get-started docs now direct users to use their configured SMTP username (default
bytesend) rather than implying only a fixed username - Core docs/readme refresh — updated main README and docs navigation/content pages for current monorepo structure and self-hosting guidance (
apps/docs/README.md,apps/docs/docs.json, local/docker/self-hosting guide pages) - Feature docs expansion — added new guides for GitHub OAuth, API authentication, plans/pricing, plan management, admin operations, and notification providers (
apps/docs/guides/*) - Mintlify branding refresh — updated
apps/docs/docs.jsontheme colors/navigation and expandedapps/docs/introduction.mdxto surface new billing, alerting, and auth capabilities
- Internal references expanded — added
.references/README.md,smtp-auth-and-operations.md,release-playbook.md, andrepository-governance.md - Webhook reference improvements — expanded
.references/webhook-architecture.mdwith operations checklist, common failure modes, and change-safety notes
- Issue form upgrades — revamped bug/marketing/SMTP templates with clearer triage metadata, reproducibility fields, and validation checkboxes
- PR labeling workflow rename — renamed the workflow file to
label-prs.yml - Label action token update — updated token reference in
.github/workflows/label.yml - Website test workflow tuning — adjusted website test workflow behavior
- Docker publish workflow update — updated
.github/workflows/docker-publish.yml - Docker manifest recreation safety — docker publish now removes existing manifests before create, preventing rerun failures on previously published tags
- Docker remote tag cleanup — docker publish now removes pre-existing remote tags/manifests with
docker buildx imagetools rmbefore publishing platform images/manifests, avoidingis a manifest listrerun failures - Website tests pnpm version alignment — removed hardcoded pnpm version from
.github/workflows/website-test.ymlso CI uses the repositorypackageManagerversion (pnpm@9.0.0) - Docker publish tag strategy hardening —
.github/workflows/docker-publish.ymlnow publishes ref-aware tags (latest,develop, version tag, and commit SHA) with matching multi-arch manifests - Manual Docker publish branch support — wired
workflow_dispatchbranch input into checkout and tag resolution so manual runs build/publish the selected branch - Labeler rules refresh — updated
.github/labeler.ymlto align automated PR labeling with the current repository structure - Actions runtime forward-compatibility — added
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=trueacross workflows to avoid Node 20 JavaScript action deprecation breakage
- Suppression removal reliability — improved suppression deletion flow to handle non-canonical casing/inputs more robustly in dashboard and backend paths
- Usage limit consistency — fixed plan usage limit handling to align dashboard/service behavior with shared plan constants
- Contact CTA destination — changed the marketing contact link from email to Discord
- Domain-service unit test import stability —
apps/web/src/server/service/domain-service.tsnow initializes DNS resolvers with runtime-safe fallbacks (promises API or callback API), preventingERR_INVALID_ARG_TYPEwhen DNS methods are partially mocked in tests - Usage unit test expectation alignment —
apps/web/src/lib/usage.unit.test.tsnow derives expected costs from exported usage constants instead of stale hardcoded values - Workspace SDK resolution in Vitest —
apps/web/vitest.config.tsnow aliasesbytesend-jstopackages/sdk/index.tsduring tests so unit suites do not depend on prebuilt SDKdistartifacts - Contact-service unit test isolation —
apps/web/src/server/service/contact-service.unit.test.tsnow mocksLimitService.checkContactsLimitto avoid transitiveTeamServicecache dependencies and prevent brittle failures - Campaign security test alignment —
apps/web/src/server/api/routers/campaign-security.trpc.test.tsupdated for current plan access expectations
- SMTP Dockerfile context compatibility —
apps/smtp-server/Dockerfileno longer expectspnpm-lock.yamlin app-only build contexts and now uses an app-local install path that works with theapps/smtp-serverDocker build context - SMTP container entrypoint correction — fixed runtime command in
apps/smtp-server/Dockerfileto executedist/server.jsfrom the container working directory - SMTP Docker Corepack compatibility — pinned Docker image pnpm activation in
apps/smtp-server/Dockerfiletopnpm@9.0.0(instead oflatest) to avoid Corepack bootstrap/runtime failures in CI builds - SMTP package manager metadata — added
packageManager: pnpm@9.0.0toapps/smtp-server/package.jsonso Corepack does not auto-inject newer pnpm versions during container installs
- SES callback SSRF hardening —
apps/web/src/app/api/ses_callback/route.tsno longer fetches user-providedSubscribeURLdirectly; it now constructs a trusted AWS SNS confirmation URL from validatedTopicArn/Tokencomponents before issuing the request - SES callback log-safety hardening — replaced ad-hoc request/parse logging in
apps/web/src/app/api/ses_callback/route.tswith constant-format structured logs to avoid tainted-format-string risks from untrusted payload fields - SPF verification sanitization fix —
apps/web/src/server/service/domain-service.tsnow parses SPF TXT mechanisms and validatesinclude:domains (amazonses.comor subdomains) instead of broad substring checks - DKIM key strength upgrade —
apps/web/src/server/aws/ses.tsnow generates 2048-bit RSA keys (up from 1024-bit) - Stripe seed secret logging removal —
packages/scripts/stripe-seed.tsno longer logs any portion ofSTRIPE_SECRET_KEY - Python webhook example exception exposure fix —
packages/python-sdk/example/webhook-test-project/receiver.pynow returns a generic verification failure message and avoids exposing exception text to clients - Workflow least-privilege permissions —
.github/workflows/website-test.ymlnow sets explicitpermissionswithcontents: read
0.2.4 - 2026-05-08
- Stripe webhook automation —
pnpm stripe:seednow registers the Stripe webhook endpoint automatically. For non-development environments it lists existing webhook endpoints, creates the endpoint if not found (or updates its enabled events if it already exists), and saves the endpoint ID andwhsec_*secret to theAppSettingtable. Development environments skip registration and print astripe listenhint instead getWebhookSecret()— new export instripe-config.tsthat resolves the webhook secret from theAppSettingtable; used as a fallback in the Stripe webhook route whenSTRIPE_WEBHOOK_SECRETenv var is not setstripe.webhookconfig keys —DB_CONFIG_KEYSinpackages/libnow includesstripe.webhook.endpoint_idandstripe.webhook.secretso the seed script and webhook route share a single source of truth for key names
extraDomainSlotscolumn — addedextraDomainSlots Int @default(0)to theTeammodel for tracking purchased additional domain slotsextraMemberSlotscolumn — addedextraMemberSlots Int @default(0)to theTeammodel for tracking purchased additional team member slots
- Cookie Policy — new
/cookie-policypage covering UK PECR-compliant cookie usage; contact:legal@nodebyte.co.uk - DMCA Policy — new
/dmcapage with designated DMCA agent and takedown procedure; primary contact:dmca@nodebyte.co.uk - Acceptable Use Policy — new
/acceptable-usepage defining prohibited use for the email platform; contact:legal@nodebyte.co.uk - Data Processing Agreement — new
/dpapage (UK GDPR Art. 28) documenting sub-processors (NodeByte Hosting UK, Amazon SES EU/US, Upstash Redis EU); contact:legal@nodebyte.co.uk /legalhub — new central legal index page listing all six policy documents (Terms, Privacy, Cookie Policy, AUP, DMCA, DPA) as navigable cards with contact email references
LimitReason.MARKETING_NOT_AVAILABLE— new enum value added toLimitReasoninlib/constants/plans.tsfor use across server guards and the upgrade modalLimitService.checkMarketingAccess(teamId)— new static method returningfalsefor Free plan teams in cloud mode; bypassed for self-hosted and admin/founder teams- Campaign API guard —
createCampaignandduplicateCampaigntRPC mutations now callcheckMarketingAccessupfront and throwFORBIDDENfor Free plan teams - Campaign service guard —
createCampaignFromApiandscheduleCampaignincampaign-service.tsnow callcheckMarketingAccessand throwByteSendApiError(FORBIDDEN)for Free plan teams
- Limit methods return usage counters — All
LimitService.check*Limit()methods now return{ isLimitReached, limit, currentCount, reason? }to enable UI display of usage ratios - Domain limit enforcement —
domain.createDomainnow enforces the domain limit (base + extraDomainSlots) at the API level and returns descriptive error with currentCount - Team member limit enforcement —
team.createTeamInviteenforces the team member limit (base + extraMemberSlots) and includes currentCount in error message - Contact limit enforcement —
ContactQueueService.addBulkContactJobschecks the contact limit before queueing imports; includes currentCount in error - Webhook limit enforcement —
WebhookService.createWebhookalready enforced limits (reconfirmed) - Contact book limit enforcement —
contactBookService.createContactBookalready enforced limits (reconfirmed)
purchaseAddonMemberSlotsmutation — new tRPC mutation (alongside existingpurchaseAddonDomainSlots) allows teams to buy additional team member slots at CA$5/slot/month- Separate addon price IDs —
PRICE_KEYS.addonnow includes bothdomainMonthlyandmemberMonthlyfor distinct pricing;createAddonCheckoutSessionacceptsaddonTypeparameter to select the correct price - Webhook addon tracking — Stripe webhook processing now distinguishes between
EXTRA_DOMAINandEXTRA_MEMBERaddon prices and updatesteam.extraDomainSlotsandteam.extraMemberSlotsindependently
- Email meter event reporting —
EmailQueueServicenow reports successful emails to Stripebilling.meterEventAdjustmentsafter each send, tracked separately by type (marketing vs transactional). Failures are logged but non-fatal so email delivery is never blocked by metering issues - Overage usage tracking — Marketing and transactional emails beyond the plan's included monthly limit are now reported to Stripe for metered billing (overage charges)
- Domain usage counter + purchase flow — "Add domain" now shows "X / Y" usage and includes an in-dialog add-on checkout CTA. When below limit, the add form is shown; when at limit, the add form is hidden and the purchase CTA + limit message are shown
- Team member usage counter + purchase flow — "Invite Member" now shows "X / Y" usage and includes an in-dialog add-on checkout CTA. When below limit, invite form is shown; when at limit, the invite form is hidden and the purchase CTA + limit message are shown
- Upgrade/purchase flow wiring — Domain/member add-on CTAs open Stripe checkout sessions with proper success/cancel URLs and metadata for webhook handling
- Database migration required — run
pnpm prisma migrate devto add theextraMemberSlotscolumn to the Team table and create the new member addon price in Stripe viapnpm stripe:seed
- Pricing plans updated — landing page pricing section (
page.tsxandPricingTiers.tsx) now shows the correct three plans: Free (CA$0, 12,500 emails/mo), Hobby (CA$5/mo, 25,000 emails/mo), and Lite (CA$10/mo, 50,000 emails/mo, recommended). Removed the old Professional and Lifetime cards - Comparison table corrected — free tier row updated from
5,000/moto12,500/mo; "Lifetime plan" row replaced with "Custom plans" (✓ for ByteSend, — for all competitors) to reflect the current offering - CTA copy corrected — "5,000 emails per month" updated to "12,500 emails per month" in the bottom CTA section
- Footer simplified — site footer reverted to minimal single-row layout (
© ByteSend · Docs · Changelog · Legal · Status) with "Legal" linking to the new/legalhub instead of individual policy links - Legal contact emails — all
hey@nodebyte.co.ukreferences inprivacy/page.tsxandterms/page.tsxreplaced withlegal@nodebyte.co.uk
- Sidebar Marketing section locked on Free plan —
AppSidebarnow checkscurrentTeam.planand, for Free plan teams in cloud mode, renders the Contacts and Campaigns items as non-navigable buttons with a lock icon and dimmed opacity. Clicking either item opens the upgrade modal. The "Marketing" section label gains a "Paid" badge - Campaigns page upgrade gate —
/campaignsrenders a centred lock screen (icon + copy + "Upgrade plan" button) instead of the campaign list when the current team is on the Free plan - Contacts page upgrade gate —
/contactsrenders the same lock screen pattern for Free plan teams - Upgrade modal message —
UpgradeModalmessages Record now includesMARKETING_NOT_AVAILABLE: "Marketing features (Contacts & Campaigns) are not available on the Free plan."
- Template/Campaign/Double opt-in editor layouts unified — all three editor pages now share the same sticky top bar, metadata strips, and canvas structure for consistent behavior
- Top-bar clipping resolved across all editor pages — removed negative vertical wrapper offsets that were causing the back/status row to clip under the dashboard header
- Editor canvas attached to metadata strip — removed the visual gap so the editor appears connected to the variables/required strip rather than floating as a separate block
- Editor width expanded to full available content area — editor canvas now spans the dashboard content width instead of constrained widths/max-width wrappers
- Theme-aware editor shell — editor wrapper, toolbar, popovers, slash command menu, and inline controls now use design tokens (
bg-background,bg-popover,border-border,text-foreground) for light/dark consistency - Toolbar feature expansion — added paragraph/heading controls, task list, text alignment (left/center/right), blockquote, code block, horizontal rule, unlink, and undo/redo actions in addition to existing inline formatting and list controls
- Migration history drift resolved —
_prisma_migrationstable contained ~50 stale records from old migrations that no longer existed locally. Truncated the table and replaced the entire history with a single baseline migration (20260507000000_init) generated from the current schema. Localprisma/migrations/folder now has exactly one migration; DB records match
0.2.3 - 2026-05-07
- Per-team custom SMTP username — teams can now set a custom SMTP username instead of the shared
bytesenddefault. The username is stored on theTeammodel (smtpUsername String?;nullmeans use the global default). Validation enforces alphanumeric characters, underscores, dots, and hyphens (max 64 chars) POST /api/v1/smtp/authendpoint — new public API endpoint the SMTP server calls duringonAuthto validate credentials. Looks up the API key, resolves the team's effective username (smtpUsername ?? SMTP_USER), and returns{valid: true}or 401. Exempt from the standard bearer-token middleware and rate limiter- SMTP server remote auth —
onAuthinapps/smtp-servernow calls the ByteSend API for credential validation instead of doing a static string comparison againstSMTP_AUTH_USERNAME. The server is fully stateless about usernames; all logic lives in the API
- Editable SMTP settings page — the SMTP credentials page (
/developer-settings/smtp) is now a client component with an editable username field. Shows the effective username (custom or default), a "Reset to default" button when a custom value is set, and an inline save button that appears when the field is dirty team.getTeamDetailstRPC query — new procedure returningid,name, andsmtpUsernamefor the current teamteam.updateSmtpUsernametRPC mutation — team admins can update or reset their SMTP username. Passingnullreverts to the global default
- Discord webhook notifications —
DISCORD_WEBHOOK_URLis now fully wired to real app events. When set, ByteSend posts a message to Discord on: new user signup (email + name), new team created (name + team ID), hard bounce detected (email ID, subject, affected recipients, team ID), and spam complaint received (same fields). All notifications fire-and-forget and never block the main request flow
All dashboard pages and components now consistently follow the design system. The following violations were corrected across 13+ files:
Status badges
email-status-badge.tsx— replaced all hardcoded Tailwind palette colors (bg-emerald-500/15 text-emerald-500,bg-purple-500/15 text-purple-500,bg-gray-400/30, etc.) with CSS variable design tokens (bg-green/15 text-green,bg-primary/10 text-primary,bg-muted/30 text-muted-foreground, etc.)domain-badge.tsx— replacedtext-emerald-600 dark:text-emerald-400 bg-emerald-500/10,text-amber-600 dark:text-amber-400 bg-amber-500/10etc. withtext-green bg-green/15,text-yellow bg-yellow/15webhook-status-badge.tsxandwebhook-call-status-badge.tsx— replacedbg-gray-700/10 text-gray-400default state and removed fixedw-[130px]/w-[110px]in favour ofmin-w-24 px-2contact-list.tsx— replaced fixedw-[130px]status pills withmin-w-24 px-2
Card and table borders
email-list.tsx,contact-list.tsx,webhook-list.tsx,suppression-list.tsx,dashboard/reputation-metrics.tsx— replacedborder shadow/border rounded-xl shadowwithborder border-border/60 rounded-xlcontact-list.tsx— fixed typoborder-broder→border-border/60
Email preview
email-details.tsx— replaceddark:bg-slate-200 h-[350px]email preview container withbg-white h-88; card borders changed fromrounded-lg shadowtoborder-border/60 rounded-xl;border-gray-300 dark:border-gray-700timeline connector →border-border
Editor pages — dark-mode canvas
double-opt-in/page.tsx— settings cardborder rounded-lg shadow→border border-border/60 rounded-xl; editor wrapperrounded-lg bg-gray-50→ same dark-aware panel (border + header + white canvas) applied to template and campaign editors in the previous release
Arbitrary Tailwind values replaced with shorthands across all modified files:
w-[700px]→w-175/max-w-175;w-[600px]→w-150;w-[300px]→w-75;w-[240px]→w-60;w-[200px]→w-50;w-[180px]→w-45;w-[150px]→w-38;w-[130px]→min-w-24 px-2;w-[110px]→min-w-24 px-2;w-[100px]→w-25;w-[80px]→w-20;w-[70px]→w-17.5;h-[350px]→h-88;h-[18px] w-[18px]→h-4.5 w-4.5;w-[300px]tooltip →w-75;-mt-[0.125rem]→-mt-0.5
0.2.2 - 2026-05-03
- Emails stuck as QUEUED forever — when
getConfigurationSetName()returnednullthe job silently returned success to BullMQ without marking the email as failed, leaving it inQUEUEDstate indefinitely. Now explicitly marks the emailFAILEDwith a descriptive error event so the problem is visible in the email log - No retry on transient SES errors — any SES error (throttle, 5xx, network hiccup) was immediately written as a permanent
FAILEDwith no retry. Transient errors (TooManyRequestsException,ServiceUnavailableException, server-fault responses) are now re-thrown so BullMQ retries the job automatically - Zero retry budget on email jobs —
DEFAULT_QUEUE_OPTIONShad noattemptskey so BullMQ defaulted to 1 attempt. Email jobs now receive 3 attempts with 10 s exponential backoff (10 s → 20 s → 40 s) - Silent worker crashes — email queue workers had no
errororstalledevent listeners; a crashed worker left jobs orphaned with no log trace. Both events are now logged with region and queue context
- DKIM auto-reregister infinite loop — the hourly background verification job triggered auto-reregistration on
wrong_keyDNS status and also whenlastCheckedAtwasnull, causing an endless re-registration cycle for newly added domains. Auto-reregister now only fires when the DKIM record is confirmedfoundin DNS (propagation confirmed) but SES has not acknowledged it for over 1 hour, and requires a non-nulllastCheckedAt - Verify button inaccessible after DKIM re-generation — reregistration set
isVerifying: true, which hid the Verify button and locked users out of the verification flow. Now setsisVerifying: falseand uses a Redis-backeddkimReregisteredflag (24 h TTL) to show an amber guidance banner with instructions; the banner clears automatically when the user clicks Verify
- Version always showing "unknown" in production — Docker Compose defaulted
APP_VERSIONto the string literal"unknown"when not provided by CI; the/api/versionroute immediately returned this sentinel without attempting the GitHub API fallback. The route now ignores"unknown"and"canary"as sentinel values, and the Docker ComposeAPP_VERSIONarg now defaults to empty so the GitHub release lookup runs when CI does not inject a version
0.2.1 - 2026-04-26
- Domains admin section — added a global domains view to the admin panel with filtering, pagination, team ownership, region, verification status, tracking flags, and verification-in-progress visibility
- Webhooks admin section — added a global webhooks view to the admin panel with filtering, pagination, owning team, creator, status, subscribed event summary, consecutive failure count, and last success/failure timestamps
- Billing admin section — added a global billing view to the admin panel with summary cards and per-team billing records covering plan, latest subscription, billing email, Stripe customer ID, period end, and cancel-at-period-end state
- Expanded admin navigation — admin tabs now include Domains, Webhooks, and Billing alongside SES Configurations, Teams, Email Analytics, and Users
- Teams and Users tables redesigned — the Teams and Users admin lists now use the same shared table primitives and muted header-row treatment as SES Configurations, replacing the older hand-rolled tables and bright blue row actions with a more consistent admin UI
- Admin status styling unified — teams and users now use compact semantic pill indicators for active/blocked and user flag states so status presentation matches the rest of the admin panel
- Mobile sidebar user section still clipped in portrait — the previous sidebar fix was incomplete on short mobile viewports. The mobile drawer now uses a viewport-bounded height (
svhwithdvhsupport), amin-h-0inner column, safe-area bottom padding, and non-shrinking sidebar header/footer regions so the user section remains visible in portrait as well as landscape - JavaScript SDK repository metadata — corrected the npm package repository URL in
packages/sdk/package.jsonfrom the old repository path tohttps://github.com/NodeByteHosting/bytesend-cloud, so published package metadata now points to the right source repository
0.2.0 - 2026-04-23
- DKIM auto-reregistration — when a DKIM TXT record is confirmed present in DNS but AWS SES has been stuck in a non-
SUCCESSstate for over an hour, the verification cycle now automatically regenerates the DKIM signing attributes and forces a fresh SES check cycle, eliminating the "delete and redo" workaround - Parallel DNS pre-checks —
refreshDomainVerificationnow runs independent DNS lookups for DKIM, SPF, and MX in parallel alongside the SES identity poll, returning adnsPrecheckresult with per-record"found" | "wrong_key" | "not_found"status - Manual DKIM re-generation — new
reregisterDkimtRPC mutation exposed in the domain Actions dropdown; generates a fresh RSA key pair, updates the stored public key, and resets DKIM status toNOT_STARTEDso the verification loop restarts cleanly - Full domain section redesign — complete Vercel-inspired overhaul of all domain pages and components:
- Domain list rewritten as a full-width column-header table (
Domain / Status / Region / Added) replacing the old card-stack layout; clickable rows with hover states and empty state illustration DomainStatusBadgeredesigned as compact pill badges with Lucide status icons (CheckCircle2, XCircle, Clock, AlertTriangle) and semantic color fills (emerald/red/amber)StatusIndicatorsimplified to a 1px self-stretch vertical line with the same semantic colors- Domain detail page: removed
max-w-4xlconstraint — layout now fills available width at all breakpoints - Domain detail page header improved: region shown inline, mobile-first
flex-col sm:flex-rowlayout, Actions + Send test buttons in a consistent toolbar - DNS status signal cards (
DKIM / SPF / DMARC) now render full-width in agrid-cols-1 sm:grid-cols-3grid with tinted backgrounds per status, icon + label + description + DNS hint in a consistent visual hierarchy - DNS records table: improved column widths,
border-b border-border/40row separators,w-55Name column,max-w-90 truncateValue cells - "Add domain" button changed to
variant="outline" size="sm"matching other dashboard action buttons; dialog form spacing tightened
- Domain list rewritten as a full-width column-header table (
- Removed framer-motion entirely — all framer-motion animations replaced with Tailwind CSS
animate-in/animate-out/data-[state]variants:email-details.tsx— fade-in replaced withanimate-in fade-in duration-300campaigns/[campaignId]/page.tsx—AnimatePresence+motion.divlayout animations removedpackages/ui/accordion.tsx— unused framer-motion import removedpackages/ui/sheet.tsx— framer-motion overlay and slide animations replaced with Radixdata-[state=open/closed]Tailwind variants
- Sheet transition animations removed — all
animate-in/animate-outslide and fade classes stripped fromSheetOverlayand allsheetVariantssides inpackages/ui/sheet.tsx; drawer now opens and closes instantly with no layout shift - Float keyframes removed —
--animate-float,--animate-float-delayed, and the@keyframes floatblock removed frompackages/ui/styles/globals.css; accordion animation duration reduced from default to0.2s - Marketing page animation and gradient cleanup — removed all gradient overlays (including the
bg-linear-to-boverlay inFeatureCard.tsx) and transition effects from the marketing page; email preview fade-in wrapper removed fromemail-details.tsx
- Geist font — switched from Inter + JetBrains Mono to Geist Sans + Geist Mono via
next/font/google; CSS custom properties--font-geist-sans/--font-geist-monowired through@themeinglobals.css - Vercel-style neutral palette — overhauled the entire color system from indigo-tinted backgrounds to pure black/gray neutrals:
- Light mode: white background, near-black text (
#171717), neutral gray borders (#e5e5e5),#f5f5f5muted surfaces - Dark mode: near-black background (
#0a0a0a),#111card surfaces,#262626borders,#737373muted foreground - ByteSend electric blue primary (213 76%/94%) unchanged as the brand accent
- Light mode: white background, near-black text (
- Full landing page overhaul — complete Vercel-inspired redesign of
apps/web/src/app/(marketing)/page.tsx:- Hero — typography scaled up (5xl → 7xl), badge updated to "Open source · Self-hostable · Free tier included", added "Read the docs" secondary CTA
- TrustStrip — full-width divider with column separators (Vercel style), updated platform stats
- DevSection (new) — split panel with value prop, feature checklist, primary/secondary CTAs on the left, and a terminal-style TypeScript SDK code snippet on the right (static
<pre>/<code>, no added dependencies) - Comparison table — redesigned from a card grid to an HTML
<table>comparing ByteSend vs Resend vs SendGrid vs Postmark vs AWS SES across key features with ✓ / — cells - CTA —
bg-muted/20alternating background, larger headline, self-hosting note with docs link
- Version route rewritten —
/api/versionnow uses a multi-source fallback cascade instead of a single GitLab call:NEXT_PUBLIC_APP_VERSIONbuild-time env var (CI sets this beforenext build)- GitLab:
permalink/latest→ releases list → repository tags - GitHub:
releases/latest→ releases list → tags "canary"fallback of last resort
- Each failed source now logs
console.errorwith context so silent failures are visible in server logs revalidateset to3600so the version is cached per-deployment rather than fetched on every request
- Breadcrumb navigation — dashboard header now renders a full breadcrumb trail built from the pathname with human-readable segment labels, replacing the single-segment display
- Settings nav tabs —
SettingsNavButtonactive indicator switched toborder-foreground; transitions updated totransition-all duration-150
- Domain detail page had duplicate legacy component definitions (
DomainSettings,DnsVerificationStatus, oldDomainItemPage) left over from a partial edit — removed all stale code packages/ui/sheet.tsxno longer depends on framer-motion for slide and fade transitions, resolving WebKit rendering performance issues on Safari- OAuth profile picture not showing in sidebar —
PrismaAdapteronly writesimageto the database on the very first OAuth sign-in viacreateUser; subsequent logins never updated a stale or missing image. ThesignIncallback inauth.tsnow reads the correct provider-specific image field on every OAuth login (avatar_urlfor GitHub,picturefor Google, CDN URL constructed fromid/avatarfor Discord) and syncs it to the DB when it differs. Thesessioncallback explicitly forwardsuser.imageso the updated value reaches the client immediately without requiring a re-login. - Sidebar mobile footer pushed off-screen —
overflow-y-autoon the mobileSheetContentwrapper broke the flex layout, causingSidebarFooter(with the user avatar and version info) to scroll off-screen on smaller viewports. Changed tooverflow-hiddenso the footer stays pinned and onlySidebarContentscrolls. - Version route always returning "canary" — the original implementation only tried a single GitLab endpoint (
permalink/latest) which requires GitLab 14.2+ with formal releases configured; all failures were silently swallowed with no logging. Replaced with the multi-endpoint cascade described above.
1.0.0-beta.1 - 2026-04-08
Initial public beta release of ByteSend — an all-in-one email infrastructure platform for transactional and marketing email delivery.
- Email magic-link authentication with 6-character code entry (always shown after submit; code submits directly to NextAuth callback — no "please click the link" dead-end)
- OAuth sign-in via GitHub, Google, and Discord
- Profile setup screen on first login (name + avatar URL) for email-only users
- Session-based team context with team switching in the sidebar
- Responsive app shell with collapsible sidebar (icon-only on desktop, sheet drawer on mobile)
- Sidebar layout fixed to viewport height (
h-dvh) so the header never scrolls away on mobile - Sidebar nav groups: General, Marketing, Settings, Admin (role-gated)
- Footer links (Feedback, Discord) moved into the scrollable content zone with
mt-autoso they never push the user info off-screen regardless of how many nav items are added - Sticky-free header bar using flex layout (always pinned, never disappears on scroll)
- Team switcher dropdown in the sidebar header with team image support
- User menu dropdown: profile links, theme switcher, log out
- Transactional email sending via AWS SES
- Real-time delivery status tracking (delivered, bounced, complained, opened, clicked)
- Per-domain email sending with configurable From addresses
- Email queue with BullMQ for reliable delivery
- Email list with pagination, filtering, and status badges
- Contact-level click and open tracking via redirect proxy
- Visual campaign editor using the ByteSend Email Editor (Notion-like WYSIWYG)
- Campaign scheduling (send now or at a future date/time)
- Audience targeting via contact books
- Per-campaign analytics (sent, delivered, opened, clicked, unsubscribed, bounced)
- Drag-and-drop image upload in the campaign editor (proxy-based, no CORS issues)
- Reusable email templates with the visual editor
- Template image upload via server-side S3 proxy
- Template duplication and management
- Contact book management (lists/segments)
- Per-contact subscription status, consent, and activity history
- Custom contact variables for personalisation
- Double opt-in flow support
- Bulk import
- Domain management with DNS verification
- Per-domain DKIM, SPF, and DMARC guidance
- Sending domain selector dropdown on emails and campaigns
- Dashboard with delivery overview chart (7-day and 30-day views)
- Bounce rate gauge
- Domain-level filter on analytics
- Open rate and click rate metrics
- API key management with scoped permissions
- REST API for transactional email sending (
/api/v1/emails) - API rate limiting (configurable via environment variable)
- Configurable webhook endpoints per team
- Event types: delivered, bounced, complained, opened, clicked, unsubscribed
- Domain-level webhook filtering
- Webhook signature verification
- Global suppression list (auto-populated from bounces and complaints)
- Manual suppression management
- Suppression export
- Team general settings (name, logo/image, delete team)
- Team member management with role-based access (Admin / Member)
- Team invite system with email notifications
- Usage & billing page
- Billing via Stripe with plan tiers: Free, Hobby, Lite, Professional, Lifetime
- Stripe metered billing for overage (transactional and marketing usage)
- Upgrade modal with plan selector
- User list with pagination
- Team list with pagination
- User ban / unban toggle
- Beta access management
- SES configuration management (add, edit, remove AWS credentials)
- Standalone SMTP server (ports 25, 587, 465)
- API key authentication
- STARTTLS and implicit TLS modes
- Forwards mail to ByteSend API
- Hot-reload TLS certificates
- Docker Compose configurations for web and SMTP
- Standalone Next.js output for Docker deployments
- PostgreSQL via Prisma ORM with full migration history
- Redis via BullMQ for job queues
- S3-compatible object storage for team images, campaign assets, and template images
- Server-side upload proxy (
/api/upload) — all file uploads go server-to-S3, eliminating browser CORS issues with object storage - Environment variable validation with
@t3-oss/env-nextjs
- Landing page with hero, trust strip, features grid, pricing cards, competitor comparison, and CTA
- Light/dark hero screenshots
- Static export (
force-static) for zero-latency page loads - AVIF + WebP image optimisation
- Mintlify-based docs site
- API reference with OpenAPI spec
- Get-started guides: Node.js, Python, Go, SMTP, Local, AWS credentials, Docker
- Guides: campaign personalisation, double opt-in, React Email integration, webhooks
- Self-hosting overview and SMTP server guide
- CORS on file uploads — Presigned S3 PUT URLs were blocked by browser CORS preflight against Hetzner Object Storage. All uploads now go through the
/api/uploadNext.js route which performs the S3PutObjectserver-side. - Login code entry did nothing —
handleVerificationCodeSubmitshowed an error telling users to click the magic link instead of verifying. Now correctly navigates to/api/auth/callback/email?token=...so manual code entry actually signs the user in. - Email lost after magic link redirect — The user's email was stored only in component state; following the magic link to
?verify=1cleared it. Email is now persisted insessionStorageand restored on mount. - Two separate post-email UIs — The
emailSentcard (info only, no code entry) and theisVerifyingcard (broken code entry) were separate. Merged into a single unified code-entry UI shown immediately after email submission. - Dashboard header disappears on scroll —
sticky top-0broke becauseoverflow-x-hiddenon the sidebar wrapper created a scroll container boundary, causing the sticky element's stacking context to disappear as the document scrolled. Fixed by switching to ah-dvh overflow-hiddenviewport pane layout where the header isshrink-0and only the<main>scrolls. - Sidebar user info pushed off-screen on mobile — Adding links to
SidebarFootergrew it beyond the viewport with no escape. Footer now only containsVersionInfo+NavUserwithshrink-0. Feedback and Discord links moved intoSidebarContentwithmt-autoso they float to the bottom of the scrollable zone and can never overflow. - Hero images returning null —
next/imagemakes a server-side request to optimise external images; the CDN (embrly.ca) blocked this with an empty response. Switched to the localpublic/hero-light.webpandpublic/hero-dark.webpassets. - SMTP server build failure — Orphaned
main().catch(...)call at the bottom ofserver.tsreferenced a function that was never defined; removed. - Create team routing — Sidebar "Create team" link pointed to
/create-team(404). Fixed to/join-teamwhich renders<CreateTeam />correctly with its own scopedTRPCReactProviderlayout. - Team name uniqueness — No uniqueness constraint existed on
Team.name, allowing duplicate team names. Added@uniqueto the Prisma schema, created migration, and added pre-flight checks increateTeamandupdateTeamservice methods. isBannedmigration drift — Column existed in the DB but not in the schema, causing Prisma Client to ignore it. AddedisBannedto the schema and regenerated the client.
- All file uploads validated server-side: type allowlist (JPEG/PNG/WebP/GIF), 5 MB max, team membership verified before accepting any upload.
- API key authentication on SMTP relay with constant-time comparison.
- Rate limiting on email auth (
AUTH_EMAIL_RATE_LIMIT) and API endpoints (API_RATE_LIMIT). - Campaign and team procedures enforce team membership at the tRPC middleware level.
- Admin and founder procedures require elevated session roles checked server-side.