Troubleshooting
Common failure modes encountered by staff and developers, and how to resolve them. Each entry follows the pattern Symptom → Likely cause → Fix.
API returns "Permission denied"
Symptom. A staff user gets a 403 from src/lib/api.ts, or the UI shows an error
toast "Permission denied".
Likely cause. One of:
- The user's role does not hold the required permission.
- The permission exists in
PERMISSIONS.mdbut no seed migration has inserted it into the database. - The user has an explicit
UserPermissiondeny overriding their role grant.
Fix.
- Confirm which permission the handler is checking — grep
requirePermission('...')insrc/lib/api.tsfor the failing route. - In the Admin → Permissions UI, inspect the user's role to see if the permission is granted. If not, either change the user's role or grant the permission at the role level (and propagate via a seed migration).
- If the permission isn't listed at all, run
SELECT * FROM "Permission" WHERE name = 'x.y';in Supabase. A missing row means the seed migration hasn't run; apply it withsupabase db push. - See Architecture → Permissions system.
Bundle-budget CI failure
Symptom. GitHub Actions CI fails with a message like
chunk "index" exceeded budget of 250kb.
Likely cause. A new dependency or eager import pushed a JS chunk past its size budget.
Fix. Identify the culprit with npm run build -- --report (or the bundle-analyzer
output in CI logs). Lazy-load heavy modules with React.lazy(), split routes, or
replace the dependency with a lighter alternative. Full guide:
Operations → Bundle budgets.
Local migrations out of sync (migration drift)
Symptom. supabase db push errors like "relation already exists" or "column does
not exist" on a fresh pull.
Likely cause. Your local Supabase DB state no longer matches the migrations folder — typically after a rebase, after pulling a migration renamed by someone else, or after manual schema edits in Supabase Studio.
Fix.
supabase db reset # drops and recreates local DB, replaying every migration
supabase db push # confirms the schema matches remote expectations
db reset is destructive to local data but safe for seeded dev data. See
Operations → Database migrations.
Chrome autofills date fields with weird suggestions
Symptom. Typing in a date input triggers Chrome's autocomplete dropdown with previously-entered values.
Likely cause. Native text inputs trigger Chrome's history-based autocomplete, and
a past version of the DateInput component did not opt out correctly.
Fix. Already resolved. The fix lives in
src/components/ui/date-input.tsx. The component sets autoComplete="off",
data-form-type="other", and uses a separate calendar-icon trigger so the text input
itself does not receive autocomplete suggestions. Relevant commits on main:
21f841e fix(ui): disable Chrome autofill suggestions on DateInput38cc4a2 fix(ui): DateInput calendar flicker — separate calendar-icon trigger from text input
If the problem returns, confirm the component is imported from
src/components/ui/date-input.tsx and not a raw <input type="date"> or <input type="text">.
Build fails locally but passes in CI (or vice versa)
Symptom. npm run build succeeds on your laptop but fails on GitHub Actions, or
the other way round.
Likely cause.
- Node version mismatch between your machine and CI. CI uses the version pinned in
.nvmrc/package.json#engines. - Missing or stale environment variables locally. Secrets are provisioned in CI but
must live in your local
.env.local. - Case-sensitive imports. macOS and Windows file systems are case-insensitive; Linux
(CI) is not.
import Foo from './foo'works locally but fails in CI if the file isFoo.tsx.
Fix.
nvm useto match the pinned Node version.- Compare
.env.examplewith your.env.localand populate missing keys from Secrets setup. - Rename imports or files so the case matches exactly.
Tests pass locally but fail in CI
Symptom. npm test green locally, red in CI.
Likely cause.
- Flaky tests relying on
setTimeoutor real timers. - Timezone-sensitive tests. Your laptop runs Asia/Kolkata; CI runs UTC.
- Migration fixture order — a test relied on data seeded by a migration that hasn't been applied in CI's fresh DB.
- Shared state between tests when running in parallel (CI runs more workers than your laptop).
Fix.
- For timezone issues, set
TZ=UTCin your local shell and re-run to reproduce. Use ISO-8601 strings ordate-fns-tzto make assertions timezone-agnostic. - For migration fixtures, ensure the test seeds its own data via factories rather than relying on ambient DB state.
- For flakiness, replace real timers with
vi.useFakeTimers().
RLS policy rejecting reads
Symptom. A query that should return data returns an empty set, or a write fails
with new row violates row-level security policy.
Likely cause. The current user's role does not grant the permission required by
the table's RLS policy, or auth_user_has_permission() is returning false for them.
Fix.
- Identify the table's RLS policy. Every sensitive table (
Booking,Customer,Agent,Supplier,Payment,JournalEntry,JournalLine,FinanceConfig) callsauth_user_has_permission('x.y'). - Run this in the Supabase SQL editor as that user:
SELECT auth_user_has_permission('x.y');— if it returnsfalse, check their role grants inRolePermission. - See Architecture → RLS.
Period-locked journal rejecting writes
Symptom. Posting a journal returns an error like "period is locked" or "cannot modify entries in a closed period".
Likely cause. The accounting period covering the journal's posting date is locked. A database trigger rejects inserts/updates to journal lines whose parent entry falls inside the locked window.
Fix. Either:
- Post the journal under a date in an open period, or
- Have a user with
finance.periods.unlockreopen the locked period from Finance → Settings → Periods, post the journal, and re-lock.
Period close (finance.periods.close) is irreversible — consult Finance before closing
end-of-year. See Finance → Journals.
Razorpay webhook 401
Symptom. Razorpay dashboard shows webhook failures with HTTP 401; payments don't auto-reconcile.
Likely cause. Signature verification is failing. Either the secret configured in
Razorpay's dashboard differs from RAZORPAY_WEBHOOK_SECRET in the server environment,
or the payload is being mutated in transit.
Fix.
- Confirm the dashboard webhook secret matches
RAZORPAY_WEBHOOK_SECRETin the Cloudflare Pages / server env. Regenerate in the dashboard and update the env var if in doubt. - Make sure the webhook URL points at the live endpoint and is not going through an HTTP-to-HTTPS redirect (redirects strip the signature header).
- See Integrations → Razorpay.
Drift test failure after adding a permission
Symptom. src/test/permissions.matrix.test.ts fails with "permission X referenced
in code but not declared in PERMISSIONS.md" or the reverse.
Likely cause. Step 1 or 2 of the permission-adding checklist was skipped.
Fix. Apply the full 5-step checklist on
Permissions catalog.
In short: add to PERMISSIONS.md §4/§5/§6, write the seed migration, reference the
permission in code, re-run npm test.