SendSetsDocs

Troubleshooting

The errors self-hosters actually hit, what causes each one, and the command that fixes it.

Search this page for the exact string you saw. Every row names the cause and the command, not a category.

I clicked register and got a 403

403 This server is not accepting new accounts. Ask an administrator for an invitation.

Nothing is broken, and you do not need to change any setting to get in.

A self-hosted instance ships with DISABLE_REGISTRATION=invite_only, which closes the sign-up form to the public. There is exactly one exemption: while the users table is empty, any signup is allowed, and that is what makes a fresh instance claimable. Once one account exists, the exemption is spent for good.

The usual sequence behind that 403 is:

  1. you ran make dev at some point, which seeds nine fixture accounts by default
  2. make dev and make up pin the same compose project, the same sendsets_postgres_data volume and the same sendsets_dev database, so those accounts are your instance's accounts
  3. make up therefore found a claimed instance, issued no setup link, and printed none
  4. the sign-up form was the only path left, and invite_only refuses it

Pick the fix that matches what you want:

You wantDo this
An account for yourself, right nowdocker compose -p sendsets exec backend sendsetsctl user create --email you@example.com --admin
To recover an account that already existsdocker compose -p sendsets exec backend sendsetsctl user reset-password --email you@example.com and open the printed URL
To let teammates inInvite them from Settings > Members and share the link. See inviting teammates
A truly fresh startmake reset && make up. This destroys the volumes, including the per-organization encryption keys
Open public signupDISABLE_REGISTRATION=false and restart. Read the consequence first: opening public signup

Not sure which state you are in?

make doctor

Newer builds return the invite-only refusal with its own machine code, registration_invite_only, and the text "This server is invite only. Ask an administrator to invite you, then open the link in the invitation to create your account." Same cause, same fixes.

First run and sign-in

SymptomCause and fix
403 This server is invite only. when registeringDISABLE_REGISTRATION defaults to invite_only on self-host. Open the invitation link that carries the token, run sendsetsctl user create --email you@example.com --admin, or set DISABLE_REGISTRATION=false and restart. See accounts and access
403 This server is not accepting new accounts.DISABLE_REGISTRATION=true. No invitation overrides it. Only sendsetsctl user create adds accounts
Signup worked once, then stoppedThe first-launch exemption applies only while the users table is empty. See the first-launch exemption
make up printed no claim linkThe database already has accounts, usually from a previous make dev. Use sendsetsctl user create --email you@example.com --admin, or make reset to start over, which destroys the encryption volume
The claim link says invalid or expired24 hour lifetime, single use, and it does not survive a Redis flush. Print a new one with sendsetsctl setup-link
/setup says the instance is already set upAn account exists, so no claim link will ever be issued. sendsetsctl user reset-password --email ..., or sendsetsctl user create --email you@example.com --admin
An invited person gets invitation_invalidThe invitation expired (7 days by default), was cancelled, was already used, or they typed a different address than the one it was issued for. Re-invite them
Single sign-on succeeds at the provider then 404sThe redirect URI must be <API_PUBLIC_URL>/v1/auth/oidc/callback, not /api/v1/.... It has to match on both sides exactly. See single sign-on
Single sign-on stopped provisioning new usersProvisioning now obeys DISABLE_REGISTRATION. Invite the person, or set SSO_AUTO_PROVISION=true and restart
No login code arrivesSelf-host does not send one (AUTH_LOGIN_CODE=off). Registration and reset codes go wherever MAIL_TRANSPORT points; under compose that is the backend log
No invitation email arrivesSame cause. Copy the link from Pending invitations and send it yourself. See without a mail relay
"Too many attempts" on password resetPassword reset is capped at 2 requests per address per 4 hours. Use sendsetsctl user reset-password instead of waiting
The passkey button is missingPasskeys need a secure context, so APP_URL must be HTTPS or a localhost address. A LAN IP over plain HTTP cannot be a relying party
Google, Apple or passkey buttons appear but never workThe sign-in screen follows GET /auth/config. If they still appear, that request is failing and the screen says which API URL it tried
Locked out of the only admin accountsendsetsctl user create --email you@example.com --admin. Every recovery command is on recovering access

The build fails

SymptomCause and fix
no space left on device, often from a random service mid-compileDocker is out of disk. Free space with docker builder prune -af and docker image prune -af, check the host has about 10 GB free, then re-run make up
failed to authorize: ... EOF while pulling a base imageA transient registry blip. Re-run make up; completed layers are cached
ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY while building the forms imageA forms/node_modules from a native make forms or make dev was shipped into the Docker build context and overwrote the image's own install, and pnpm will not recreate it without a terminal. Current checkouts keep every node_modules out of the context and run pnpm in CI mode; on an older checkout, rm -rf forms/node_modules and re-run make up
failed to xattr /path/._something: operation not permitted on macOSThe checkout is on a filesystem without native extended attributes (exFAT, NTFS or a network share), so macOS writes ._* sidecar files that BuildKit cannot read. Run dot_clean -m . then find . -name '._*' -delete and re-run. Cloning to an APFS volume avoids it

The stack is up but something is wrong

SymptomCause and fix
The backend exits at bootRead the first log lines. The usual causes are a published default secret with APP_ENV=prod (the message names ALLOW_INSECURE_DEFAULTS), cloud provider defaults outside compose, a CREDENTIALS_ENCRYPTION_KEY that is not exactly 64 hex characters, or a missing EMAIL_ADDRESS, EMAIL_NAME or GEODB_PATH
The dashboard loads but every request failsCheck http://localhost:5173/config.js returns the right API_URL. It is generated at container start from SENDSETS_API_URL. If the URL is right, the origin is probably missing from CORS_ALLOW_ORIGINS
Nothing updates live and presence is emptyAUTH_SECRET must equal realtime's JWT_SECRET, and PUBSUB_ENABLED must agree across backend, consumer and realtime. Compare the fingerprints under Instance > Configuration > Environment
Workers or tracking get 401sINTERNAL_API_TOKEN must match on the backend, the workers (as ENCRYPTED_KEYS_WORKER_TOKEN) and tracking. Unset fails closed
"No mailbox workers are available" when connecting a mailboxNo worker has a heartbeat inside the last 10 minutes. Check make status shows worker running and make logs worker is clean. An empty ENCRYPTED_KEYS_BACKEND_URL or worker token lets a worker start and never register, silently
Connecting a mailbox fails with SERVER_UNREACHABLE on a reachable hostThe security setting does not match the server. A server expecting STARTTLS looks unreachable to a client attempting implicit TLS, and vice versa. Any port from 1 to 65535 is accepted, so the port alone no longer decides: set Security to SSL / TLS for a server that is encrypted from the first byte (usually SMTP 465, IMAP 993) and STARTTLS for one that upgrades in place (usually SMTP 587 or 2525, IMAP 143)
A local relay such as Proton Bridge will not connect, and the None security option is not thereIt appears only for a loopback literal (localhost, 127.0.0.0/8, ::1) on a self-hosted instance, because the mode is safe only when the credentials never leave the machine. A hostname that resolves to 127.0.0.1 is refused; type the address itself. On hosted SendSets the worker is not your machine and cannot reach a Bridge at all. See local mail relays
A mailbox reports INSECURE_REMOTE_HOSTIt is stored with the unencrypted security mode but its host is not this machine, so the worker refused to dial it rather than put the password on a wire. Reconnect it with SSL / TLS or STARTTLS, or point it back at the local relay
Connecting an Outlook mailbox stops at "Approval required" with AADSTS90095The tenant needs an admin to consent once, or the app registration asks for a permission it should not. Grant consent under Enterprise applications > your app > Permissions > Grant admin consent, and check the registration lists only the delegated User.Read, Mail.Send, Mail.ReadWrite and offline_access: IMAP.AccessAsUser.All and SMTP.Send are admin-consent-only, are not used, and refuse every non-admin on their own. If a non-admin is still refused after a tenant-wide grant, the instance predates the fix that stopped sending prompt=consent, which made Entra ID re-check consent eligibility per sign-in instead of honouring the grant; upgrade it
A mailbox stalls after about an hourThe worker is missing BOX_GOOGLE_* or BOX_OUTLOOK_*. The backend starts the OAuth flow but each worker refreshes the token. Set them and restart the worker
Sending fails with an authentication error but the password is rightSendSets negotiates the sign-in method from what the server advertises. If the mailbox reports AUTH_UNSUPPORTED, the server offers only mechanisms SendSets does not implement, such as NTLM or GSSAPI, or it offers no encrypted connection at all and SendSets will not send the password in the clear; an app password, or the provider's documented SMTP host, usually resolves both
A send is refused and not retriedA SEND_REJECTED or RECIPIENT_REJECTED error means the receiving server answered with a permanent 5xx, so retrying cannot deliver the message and would only spend the mailbox's daily budget. Both carry the server's own words, which is what distinguishes an address that no longer exists from one blocked by a policy. A temporary 4xx is retried automatically and reported as a connection problem
An IMAP mailbox connects but no mail ever arrivesCheck the sync card in the mailbox drawer for a folder count. Zero folders on a reachable server means the server refused STATUS for every folder; the worker log names each one it skipped. A connected mailbox whose inbox is genuinely empty is normal
A mailbox shows an error that is already fixedConnection errors clear themselves: the first sync pass that reaches the server again resolves them. Errors that need you to act, such as wrong credentials or a domain-authentication refusal, stay until you reconnect the mailbox or fix the cause
An IMAP mailbox stopped syncing and the log is quietSessions dropped by the server, or by a firewall that removed the mapping without closing the connection, are re-dialed on the next pass, and every pass that cannot reach the server is retried on a widening interval up to five minutes. If a mailbox is still stuck, make logs worker shows the folder cursors; a mailbox held by the sync budget says so in its drawer instead
A folder is missing from the uniboxUp to 100 folders per mailbox are synced (Instance > Configuration > Effective limits). Past that, the inbox and the special folders are kept and the rest follow the server's order, and the mailbox drawer's Sync card names how many were left out. Gmail's All Mail, Starred and Important are label views over other folders and are deliberately never synced. A folder can also be skipped when the mail server lists the same folder name twice, which the Sync card says as well. A folder sharing its UIDVALIDITY with another folder is no longer a reason to skip anything: folders are identified by name, which is what IMAP guarantees is unique. Both notes clear themselves on the next pass once the cause is gone
Scheduled sends never fireDelayed sends run through the in-process Postgres task poller (TASKS_PROVIDER=local), so the backend must be running
Every send dead-letters with permission denied on /data/blobsThe blobs volume was created before the images owned that path, so it is still root:root while the services run as uid 1000. Fix it once with docker compose -p sendsets exec -u root backend chown -R sendsets:sendsets /data/blobs. The blob_fs_root health check reports it, and volumes created from current images are already correct
email account <id> not found in workerThe mailbox is assigned to a worker that no longer exists, usually because the worker was recreated and came back with a fresh UUID. The reconciler releases and re-places it on a live worker within its interval. Compose workers now keep their id in the worker_state volume (WORKER_STATE_DIR), so this stops recurring once that volume exists; removing the volume or unsetting WORKER_STATE_DIR reintroduces the churn. See worker identity
Opens and clicks never recordFirst check the container is actually up with docker compose -p sendsets ps -a: a dead tracking breaks nothing else, because sends do not wait on it. Then check TRACKING_DOMAIN resolves and the service answers on /health. If you overrode KAFKA_TRACKING_TOPIC, it has to be overridden for the Rust publisher and the Go subscriber together
tracking exits immediately with Bind for 0.0.0.0:3000 failed: port is already allocatedSomething else on the host owns port 3000, a very common default. Set TRACKING_PORT=3001 in .env, re-run make up, and point your reverse proxy's tracking host at the new port
The backend restart-loops with duplicate migration fileTwo migrations on the branch share a version, which stops golang-migrate before a single one runs. Renumber the one that has not been released yet to the next free version and re-deploy. make check-migrations reports it, and CI runs the same check on every pull request. See adding a migration
Seeding fails with no migration found for version NThe seed image is older than your schema. Re-run with --build
A whole workspace stopped sending and its banner says suspendedIts abuse posture reached suspended. Open the org in the admin panel, read the findings in the Abuse posture panel, then retract the ones that are wrong or set the posture directly. See workspace abuse posture
Worker heartbeat offlineNothing connects to a node, so this is always diagnosed on the machine itself: systemctl status sendsets-worker, then check it can reach the backend URL, NATS or Kafka, and Redis. sendsetsctl fleet show <id> gives the last time it was seen

Recovery

Every recovery command, in all four runtimes, is on recovering access: instance status, listing admins, resetting a password through a printed one-time link, creating a new owner and admin, clearing a lost authenticator, and reissuing the first-run claim link.

The destructive alternative is make reset, which removes the volumes and starts over.

make reset destroys the encryption volume

The volumes hold the per-organization data keys. Every sealed mailbox credential and every stored message body becomes permanently undecryptable, even if you restore a database dump afterwards.

Do not flush Redis to clear a rate limit

redis-cli FLUSHDB also destroys bootstrap:setup_token, every pending auth session and every login attempt counter. On an unclaimed instance that throws away the only way in. Delete the specific key, or wait out the window.

Collecting errors somewhere you can read them

make logs shows you what a service printed. It does not tell you that a handler threw at 3am, in which build, for which organization. Every runtime in SendSets can send that to an error tracker instead, and none of them do until you say where.

There are two backends. PostHog is the default:

# backend, consumer, worker, forms, tracking, realtime
POSTHOG_KEY=phc_...
# the dashboard, the admin panel, and public form pages (browser errors)
SENDSETS_POSTHOG_KEY=phc_...
# both, when your project is not on PostHog Cloud US
POSTHOG_HOST=https://eu.i.posthog.com
SENDSETS_POSTHOG_HOST=https://eu.i.posthog.com

Sentry is still supported, on its own or alongside PostHog:

SENTRY_DSN=https://<key>@<host>/<project>
SENDSETS_SENTRY_DSN=https://<key>@<host>/<project>

Set both backends and every error goes to both, which is the safe way to try one without giving up the other. Going the other way, POSTHOG_ERROR_TRACKING=false keeps a PostHog key for product analytics and reports no exceptions there.

Under the installer's compose file these go in .env: every service inherits them through env_file, so a couple of lines and a docker compose up -d covers the whole instance.

Every event is tagged with the service that raised it, the environment and the build, so one project can hold the whole instance and still be filterable. Exceptions also carry the workspace and user they happened to and, where there was one, the failing request's id and route, so a support message quoting a request id and an issue in error tracking are the same incident. What each property is and where it comes from is on configuration. Server-side services take the environment from APP_ENV. The dashboard and admin panel take theirs from SENDSETS_SENTRY_ENVIRONMENT, since the container serving the bundle is not the process reporting the error, and the name is that one whichever backend receives the event. Hosted form pages are the exception among the browser apps: their environment is stamped into the page by the forms service, so it comes from that service's APP_ENV. The frontends are separate projects' worth of events in practice; give the browser ones their own project if you want the noise apart.

The server behind either is your choice. PostHog Cloud's free tier covers 100k exceptions a month and POSTHOG_HOST points at a PostHog you run yourself; Sentry Cloud's free Developer plan covers 5k errors a month, and a self-hosted Sentry or a Sentry-compatible server such as GlitchTip works with the same DSN variable.

Browser stack traces are minified unless the images were built with source-map upload configured, which the published ones are. See source maps if you build your own.

Configuring neither backend is a supported configuration, not a degraded one: no host is contacted and errors go to the log as before. See data control.

Still stuck

make logs            # follow everything
make logs backend    # follow one service
make doctor          # the full health check set, exits non-zero on an error

Ask in Discord or open a GitHub issue with the output of make doctor and the first 50 lines of the failing service's log.

See also

On this page