Files
warmbly/internal/app/instanceconfig/limits.go
T
Matthew Meszaros 734cb5fe08 feat: make self-hosted onboarding survivable by fixing invite_only, which could not onboard anyone (the accept route is JWT-only, so redeeming the invitation that would create your account required already having one, making the self-host default silently identical to fully closed), threading the invitation token through registration so an invited person lands in the inviting organization instead of a stray workspace, gating SSO just-in-time provisioning behind DISABLE_REGISTRATION (it bypassed the gate entirely, so an instance set to true was still open to anyone the IdP would assert) with SSO_AUTO_PROVISION as the opt-out, correcting the OIDC redirect URL that pointed at /api/v1 against a route at /v1 and 404'd every SSO login, scoping the first-launch exemption so it no longer overrides an explicit lockdown, preserving the remaining TTL when restoring a losing setup token so a public endpoint cannot hold the claim window open forever, replacing a generic 403 with typed registration_invite_only, registration_closed, invitation_invalid, setup_token_invalid and setup_already_complete codes that name the next step, logging why no claim link was issued on an already-claimed instance instead of staying silent, adding a warmblyctl operator CLI (status with health checks and a non-zero exit, reissuable setup-link, user create/list/reset-password/grant-admin/revoke-admin/disable-2fa, hash-password) so a locked-out operator no longer needs hand-written psql, adding read-only instance configuration over 104 environment variables with structural secret redaction and fingerprints, 35 health checks, a database-backed settings tier for the three keys no environment variable owns, hiding the signup form when the config already says invite_only rather than failing the whole form with a toast, and documenting first run, accounts and access, configuration, instance health and troubleshooting alongside the root .env.example the README told operators to write but never shipped (#114)
2026-08-16 05:58:11 +02:00

108 lines
5.8 KiB
Go

package instanceconfig
import (
"strconv"
"github.com/warmbly/warmbly/internal/config"
)
// LimitEntry is one number an operator may be looking for.
type LimitEntry struct {
Name string `json:"name"`
Value string `json:"value"`
Unit string `json:"unit"`
Description string `json:"description"`
}
// LimitGroup is a rendered section of the effective-limits page.
type LimitGroup struct {
Title string `json:"title"`
Entries []LimitEntry `json:"entries"`
}
// Limits is the effective-limits document. Every value is a compiled constant
// from internal/config/constants.go, so this page is read-only by construction:
// per-organization variance goes through the admin override flow instead.
func Limits() []LimitGroup {
return []LimitGroup{
{
Title: "Sending",
Entries: []LimitEntry{
{"Default campaign cap", n(config.CampaignLimitDefault), "emails/day", "Per mailbox, before any organization override."},
{"Minimum gap between sends", n(config.MinWaitTimeDefault), "seconds", "Per mailbox. Spacing matters more than throughput."},
{"Highest campaign cap accepted", n(config.LimitMax), "emails/day", "The largest per-mailbox cap the API will store."},
{"Lowest campaign cap accepted", n(config.LimitMin), "emails/day", "The smallest per-mailbox cap the API will store."},
{"Campaign ramp start", n(config.CampaignRampStartDefault), "emails/day", "Where a new campaign sender starts before the ramp lifts it."},
{"Campaign ramp increment", n(config.CampaignRampIncrementDefault), "emails/day", "Added each day, applied only as a lower bound against the mailbox cap."},
{"Campaign ramp ceiling", n(config.CampaignRampCeilingDefault), "emails/day", "Where the ramp stops lifting."},
{"Sender weight ceiling", n(config.CampaignSenderWeightMax), "weight", "The largest share one mailbox can take in a rotation."},
{"New leads per campaign", n(config.CampaignMaxNewLeadsMax), "contacts", "The most contacts one campaign can pull in per run."},
},
},
{
Title: "Warmup",
Entries: []LimitEntry{
{"Warmup start", n(config.WarmupBaseDefault), "emails/day", "Where a mailbox begins warming."},
{"Warmup ceiling", n(config.WarmupMaxDefault), "emails/day", "Where the warmup ramp stops."},
{"Warmup ramp", "+" + n(config.WarmupIncreaseDefault), "emails/day", "Added each day while the mailbox stays healthy."},
},
},
{
Title: "Organization hard caps",
Entries: []LimitEntry{
{"Connected mailboxes", n(config.HardCapMailboxes), "per organization", "The backstop when neither a plan nor an override sets one."},
{"Campaigns created", n(config.HardCapCampaignsTotal), "per organization", "Total campaigns ever created."},
{"Active campaigns", n(config.HardCapCampaignsActive), "per organization", "Running at the same time."},
{"Team members", n(config.HardCapTeamMembers), "seats", "Members in one organization."},
{"Contacts", n(config.HardCapContacts), "per organization", "Stored contacts."},
{"Campaign sends", n(config.HardCapDailyCampaignSends), "emails/day", "Across every mailbox in one organization."},
},
},
{
Title: "Daily creation throttles",
Entries: []LimitEntry{
{"New campaigns", n(config.DailyThrottleNewCampaigns), "per organization/day", "Resets at UTC midnight. Not raisable per organization."},
{"Newly connected mailboxes", n(config.DailyThrottleNewMailboxes), "per organization/day", "Resets at UTC midnight."},
{"New workspaces", n(config.DailyThrottleNewOrgs), "per owner/day", "Resets at UTC midnight."},
},
},
{
Title: "Scheduled and undo sends",
Entries: []LimitEntry{
{"New scheduled sends", n(config.DailyThrottleNewScheduledSends), "per user/day", "Rolling 24 hour window."},
{"Pending scheduled sends", n(config.MaxPendingScheduledSendsPerUser), "per user", "Queued at once. Bounds queue storage rather than cost."},
{"Undo send window", n(config.UndoSendSecondsDefault), "seconds", "Default hold before an instant send leaves. Range " + n(config.UndoSendSecondsMin) + " to " + n(config.UndoSendSecondsMax) + "."},
},
},
{
Title: "Webhooks and integrations",
Entries: []LimitEntry{
{"Dispatch floor", n(config.WebhookDispatchBasePerMinute), "events/minute", "Every organization gets at least this, including free ones."},
{"Added per allowed mailbox", n(config.WebhookDispatchPerMailboxPerMinute), "events/minute", "The cap scales with the plan's mailbox allowance."},
{"Dispatch ceiling", n(config.WebhookDispatchMaxPerMinute), "events/minute", "The hard ceiling for any plan."},
},
},
{
Title: "Content and inbox",
Entries: []LimitEntry{
{"Email body", n(config.MaxEmailBodySize / 1024), "KB", "The largest body the platform stores."},
{"Contact record", n(config.MaxContactSize / 1024), "KB", "The largest single contact payload."},
{"Mailbox folders synced", n(config.MaxEmailFolders), "folders", "Per mailbox."},
{"IMAP fetch batch", n(config.ImapFetchBatchSize), "messages", "How many envelopes one sync window holds in memory."},
{"Unibox page size", n(config.UniboxLimitDefault), "messages", "Default page size, range " + n(config.UniboxLimitMin) + " to " + n(config.UniboxLimitMax) + "."},
{"Sequence step delay", n(config.SequenceWaitAfterMax), "days", "The largest per-step wait a sequence can store."},
{"Sequence subject", n(config.SequenceSubjectLimit), "characters", "Per step."},
{"Sequence body", n(config.SequenceBodyLimit), "characters", "Per step."},
},
},
{
Title: "Notifications",
Entries: []LimitEntry{
{"Email digest window", n(config.NotificationEmailWindowDefaultMinutes), "minutes", "Default hold before notifications flush as one email. Range " + n(config.NotificationEmailWindowMinMinutes) + " to " + n(config.NotificationEmailWindowMaxMinutes) + "."},
},
},
}
}
func n(v int) string { return strconv.Itoa(v) }