Skip to content

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:

  1. The user's role does not hold the required permission.
  2. The permission exists in PERMISSIONS.md but no seed migration has inserted it into the database.
  3. The user has an explicit UserPermission deny overriding their role grant.

Fix.

  • Confirm which permission the handler is checking — grep requirePermission('...') in src/lib/api.ts for 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 with supabase 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 DateInput
  • 38cc4a2 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.

  1. Node version mismatch between your machine and CI. CI uses the version pinned in .nvmrc / package.json#engines.
  2. Missing or stale environment variables locally. Secrets are provisioned in CI but must live in your local .env.local.
  3. 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 is Foo.tsx.

Fix.

  • nvm use to match the pinned Node version.
  • Compare .env.example with your .env.local and 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.

  1. Flaky tests relying on setTimeout or real timers.
  2. Timezone-sensitive tests. Your laptop runs Asia/Kolkata; CI runs UTC.
  3. Migration fixture order — a test relied on data seeded by a migration that hasn't been applied in CI's fresh DB.
  4. Shared state between tests when running in parallel (CI runs more workers than your laptop).

Fix.

  • For timezone issues, set TZ=UTC in your local shell and re-run to reproduce. Use ISO-8601 strings or date-fns-tz to 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) calls auth_user_has_permission('x.y').
  • Run this in the Supabase SQL editor as that user: SELECT auth_user_has_permission('x.y'); — if it returns false, check their role grants in RolePermission.
  • 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.unlock reopen 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_SECRET in 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.