Skip to content

Troubleshooting

Magic-link emails never arrive. Verify SMTP end-to-end with php bin/send-test-mail.php you@example.com (see operations.md) using the exact same mailer path as real sign-in. If the test mail fails, the problem is your smtp.* config, not the app. Check spam folders and SPF/DKIM/DMARC on your sending domain if the test mail succeeds but real recipients don’t see it.

“Too many attempts” / sign-in rate limited. The magiclink:email (3/hour) and magiclink:ip (5/hour) buckets in the rate_limits table are working as intended. During authorized manual testing you can clear a specific bucket:

DELETE FROM rate_limits WHERE bucket LIKE '%magiclink%';

Never do this to bypass limits on real traffic.

A user I expect to be admin isn’t. admin_emails in config.php only grants is_admin on that user’s first login — adding an address after they’ve already signed in has no retroactive effect. Grant admin/owner status via the account’s membership/roles UI instead, or a direct database update for the very first bootstrap.

Installation

Fresh install is missing tables (accounts, invites, api_tokens, …) after running mysql < db/schema.sql. Expected — db/schema.sql is only the pre-tenancy baseline. Run the migration runner (php bin/migrate.php) to bring the schema up to date; see installation.md.

Boot-time HTTP 500 with a message about routing_mode/SPA capability. Votepit\SpaCapabilities refuses to boot if config.php sets routing_mode: 'cloud' while the built SPA doesn’t have account-prefixed client routes. Self-host installs should never set routing_mode: 'cloud' — leave it at the default 'self-host'. If you do run a Cloud-style deployment, make sure the SPA build you deployed matches the routing mode you configured.

app_key or identity_server_key left empty. These are required secrets, not optional toggles — leaving either blank is a misconfiguration. Generate both with php -r "echo bin2hex(random_bytes(32));" (run separately, they must differ) before going to production.

Fresh install failed

php bin/migrate.php stops with “Migration ’…’ failed”. The runner applies migrations in order and stops at the first failure. Its message lists three things: which migrations were applied in this run (they are committed and tracked, not rolled back), which were not attempted, and that the failing one is not tracked as applied. MySQL and MariaDB commit DDL statements implicitly, so the failing migration may have changed the schema partly.

What to do, in order:

  1. Read the error text after “failed:”. Typical causes: the database server version (see the minimum in installation.md), missing privileges for the database user (section 3 of the installation guide lists the grants), or a database that is not empty and holds objects from another application.
  2. For a new, empty database (the usual case): drop it, recreate it empty as described in installation.md, fix the cause, and run php bin/migrate.php again. This is the simplest and safest recovery.
  3. For a database with data: restore the backup you took before running the migrations, then fix the cause and run the migrations again. Do not try to edit the schema by hand and do not edit an already applied migration file (migrations are checksum-tracked).

Re-running php bin/migrate.php without fixing the cause fails at the same migration again. It is safe to re-run after a fix: applied migrations are skipped, the failed one is retried.

Duplicate detection

Duplicate search returns no/weak results. If your MySQL/MariaDB version or table engine doesn’t support InnoDB FULLTEXT, the app falls back to a pure-PHP comparison automatically — this is slower and may feel less precise on large boards, but is not an error state. Confirm your ideas table’s storage engine is InnoDB with a FULLTEXT index on title if you expect the FULLTEXT-backed path.

Agent API / MCP

401/403 from /api/v1/* or /api/v1/mcp. Confirm the Authorization: Bearer <token> header is present and the token hasn’t been revoked. 403 insufficient_scope means the token’s grant for that board is too low (write for votes/comments/new ideas, moderate for status/tags/merge and pending ideas); 403 token_issuer_not_moderator means the user who issued a moderate token no longer moderates the account — issue a new token. A token bound to one board will always get 401/403/404 against another board’s data — this is by design, not a bug (see api-reference.md).

Token creation fails / not offered in the UI. Agent API token creation is gated by the injected PlanPolicy::agentApiAllowed() (see architecture.md); Community’s own default, UnrestrictedPlanPolicy, always allows it, so this only applies if a privately hosted extension plugs in a restrictive policy.

429 from the Agent API or MCP endpoint. REST and MCP usage on one token share the same apitoken:read/apitoken:write buckets (see mcp-server.md) — a burst of MCP tool calls can exhaust the budget a REST integration also depends on, and vice versa.

MCP client gets a JSON-RPC error instead of a tool result. Check the error.code: -32700 malformed JSON, -32600 invalid request or an attempted batch call (unsupported), -32601 unknown method/tool, -32602 bad tool arguments, -32000 rate limited. Every response — success or error — is HTTP 200; don’t rely on the HTTP status code to detect a JSON-RPC-level failure.

General

Something in this documentation contradicts the code. The code is the source of truth — please report the mismatch (see SECURITY.md/repository contribution info) rather than trusting the prose over src/.