TROUBLESHOOTING.md β Known Issues, Gotchas & Fixes
Purpose: This file documents known issues, edge cases, and non-obvious behaviors in the codebase. AI agents should read this to avoid re-introducing bugs or spending time debugging known problems.
π¨ Critical Issues
1. Dashboard layout nested <html> tags β Fixed
- Where: Was
src/app/dashboard/layout.tsx(old path). - Status: β
Fixed β dashboard shell is now
(dashboard)/layout.tsxand does not re-declare<html>/<body>.
2. Duplicate NextAuth configuration
- Where:
src/lib/auth.tsANDsrc/app/api/auth/[...nextauth]/route.ts - Problem: Google provider + callbacks are defined in BOTH files independently, with slightly different callback logic. The API route file doesn't import from
auth.ts. - Impact: Behavior discrepancies between session handling and API route auth.
- Fix:
route.tsshould importauthOptionsfrom@/lib/authinstead of redefining it. - Status: β οΈ Known, not yet fixed.
3. Hardcoded backend URLs in frontend
- Where: Multiple frontend components
- Problem:
http://localhost:8000andhttp://127.0.0.1:8000are hardcoded instead of usingprocess.env.NEXT_PUBLIC_API_URL. - Impact: Breaks when deployed behind Traefik or to production.
- Fix: Replace all hardcoded URLs with
process.env.NEXT_PUBLIC_API_URL. - Status: β οΈ Known, not yet fixed.
4. Middleware session gate β Fixed
- Where:
heritage_graph_ui/src/middleware.ts - Behavior: Redirects unauthenticated users from
/curation,/platform-admin,/moderate,/account,/notification,/progression, and/community/reviewer-requestto/auth/login./contributeis gated byRequireAuthin(dashboard)/contribute/layout.tsx(client-side, after OAuth callback). - Note: Public browse routes (
/knowledge/*,/graphview,/atlas, etc.) remain reachable without login; APIs still enforce permissions. - Status: β Implemented.
β οΈ Configuration Gotchas
Sign-in: BACKEND_SYNC / BACKEND_HANDSHAKE_NOT_FOUND after Google OAuth
- Where: NextAuth
signIncallback inheritage_graph_ui/src/lib/auth.tscalls DjangoGET /data/api/testme/withAuthorization: Bearer <Google access or ID token>. - Symptoms: Google succeeds, then redirect to
/auth/login?error=BACKEND_SYNC(orBACKEND_HANDSHAKE_NOT_FOUNDfor404). - What to do:
- Read Django/API logs for the HTTP status on
/data/api/testme/(400 often meansDisallowedHost; 404 means routing or wrongINTERNAL_BACKEND_URLpath). - On the frontend service, set
INTERNAL_BACKEND_URLto an internal base URL the Node server can reach (e.g.http://backend:8000in Compose), not a public URL that redirects and stripsAuthorization. - Ensure
ALLOWED_HOSTSincludesbackendandGOOGLE_CLIENT_IDmatches on Django and Next.js (auth/AUTH.md,deployment/DOKPLOY.md). - Next.js logs a snippet on failure:
[next-auth] Django handshake non-OK response.
5. ROOT_URLCONF is "urls" not "heritage_graph.urls"
- Where:
heritage_graph/settings/base.py - Problem: Looks wrong but is correct β Django's working directory in Docker is
/app(which isheritage_graph/), sourls.pyis a top-level module. - Impact: If you run Django outside Docker with a different working directory, URL routing breaks.
- Fix: Not needed β this is intentional. But if running locally, ensure you
cd heritage_graphfirst, or setPYTHONPATHappropriately.
6. settings.py vs settings/ β two settings systems
- Where:
heritage_graph/settings/settings.pyANDheritage_graph/settings/__init__.py - Problem:
settings.pyis a legacy standalone settings file. The__init__.pydispatches todevelopment.pyorproduction.py. Both exist and can cause confusion. - Impact:
DJANGO_SETTINGS_MODULEshould besettings(dispatches viasettings/__init__.pyonDJANGO_ENV) orsettings.development/settings.productiondirectly. The legacysettings/settings.pystartproject file was removed. - Fix: Use
__init__.pydispatch (setDJANGO_ENV=developmentorDJANGO_ENV=production). Don't usesettings.pydirectly.
7. WSGI/ASGI DJANGO_SETTINGS_MODULE mismatch
- Where:
heritage_graph/wsgi.pyandheritage_graph/asgi.py - Problem: Historically
manage.pyforcedsettings.developmentwhile WSGI usedheritage_graph.settings, skipping__init__.pydispatch. - Fix:
manage.pynow defaults toDJANGO_SETTINGS_MODULE=settings(loadssettings/__init__.py+DJANGO_ENV). Gunicorn still usesheritage_graph.settingswithPYTHONPATH=/app; both resolve to the same dispatch package. - Status: Fixed for CLI (
manage.py); WSGI/ASGI unchanged and aligned in intent.
8. Duplicate CommonMiddleware in MIDDLEWARE
- Where:
heritage_graph/settings/base.py - Problem:
django.middleware.common.CommonMiddlewareappears twice in the MIDDLEWARE list. - Impact: Minor β Django handles it, but it processes requests/responses twice through CommonMiddleware.
- Fix: Remove the duplicate.
- Status: Fixed (duplicate removed from
MIDDLEWARE).
9. Legacy auth files with outdated names
- Where: ~~
heritage_graph/apps/heritage_data/clerk_auth.py~~ (removed) - Problem: This legacy file contained old Clerk authentication code. The active auth class is
GoogleTokenAuthenticationinauthentication.py. - Impact: Confusing for developers. AI agents might look for auth code in the wrong file.
- Fix: Delete
clerk_auth.pysinceauthentication.pynow handles all auth via Google OAuth. - Status: Fixed (file removed).
10. InconsistentMigrationHistory during make setup / migrate
- Error:
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency users.0001_initial - Cause: Djangoβs
django_migrationstable saysadmin.0001_initialran, butusers.0001_initial(required for the customAUTH_USER_MODEL) is not recorded as applied. Typical cases: - Stale SQLite dev DB from before the custom user model.
- PostgreSQL volume reused after a failed or partial migrate, or a DB restored from another environment.
Fix β local SQLite (development)
make reset-dev-db
Or manual:
mv heritage_graph/db.sqlite3 heritage_graph/db.sqlite3.bak-$(date +%Y%m%d-%H%M%S)
make migrate
Fix β PostgreSQL / Docker / Coolify (production or shared DB)
A. No data to keep (empty or disposable DB) β simplest:
- In Coolify (or
docker compose), remove the Postgres volume for this stack, or run:
docker exec -it <postgres_container> psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
- Redeploy the backend so
migrateruns on a clean schema.
B. You need to keep data β repair history, then migrate (get a backup first):
- Open a shell on the Postgres container and run SQL (adjust user/db names):
-- Remove the bogus admin row so Django can apply users.* then admin.* in order
DELETE FROM django_migrations WHERE app = 'admin';
- From the backend container:
python manage.py migrate --noinput
- If
migrateerrors with βrelation users_user already existsβ (table present but migration row missing), align with fakes (only if the schema matchesusers.0001_initial):
python manage.py migrate users 0001 --fake-initial
python manage.py migrate --noinput
If problems persist, compare django_migrations rows for users and admin with a known-good fresh migrate on a throwaway database.
Automated repair (Docker / Dokploy)
For Dokploy, docker-compose-dokploy.yml sets MIGRATION_AUTO_REPAIR=1 on the backend so the entrypoint runs python manage.py repair_migration_history before migrate. The command is defined in apps/heritage_data/management/commands/repair_migration_history.py. You can run it manually or set the same env on other compose stacks. Set MIGRATION_AUTO_REPAIR=0 once the DB is healthy if you want to skip the check.
π Behavioral Quirks
11. UserStatistics auto-updates only on Submission save
- Where:
heritage_data/signals.py - Problem: The
post_savesignal onSubmissionrecalculatesUserStatistics, but there's no signal forCulturalEntitysaves. - Impact: If using the new
CulturalEntityworkflow,UserStatisticswon't update. - Fix: Add a
post_savesignal forCulturalEntitythat also recalculates statistics. - Status: Fixed β
refresh_user_stats()aggregates submissions + cultural entities; both models trigger it.
12. PersonRevision auto-creation fires on every save
- Where:
cidoc_data/signals.py - Problem: The
post_savesignal creates aPersonRevisionon everyPerson.save(), even if no fields changed. - Impact: Could create unnecessary revision records.
- Fix: Compare old and new field values before creating revision.
13. Submission submission_id is auto-generated
- Where:
heritage_data/models.pyβSubmission.save() - Problem:
submission_idis generated asrandom.choices(string.ascii_uppercase + string.digits, k=11)β not guaranteed unique (though collisions are rare with 11 chars from 36-char alphabet). - Impact: Very unlikely collision, but not enforced at DB level beyond
unique=True(which would raise an IntegrityError).
14. Frontend .env.local is in wrong location
- Where: Should be at
heritage_graph_ui/.env.local - Problem: Next.js expects
.env.localat the project root (next topackage.json). If it's elsewhere, env vars won't load. - Fix: Ensure
.env.localis inheritage_graph_ui/directory.
π³ Docker Issues
15. Google OAuth requires correct redirect URIs
- Where: Google Cloud Console β API Credentials
- Problem: Google OAuth will reject sign-in attempts if the redirect URIs don't match exactly. For development, you need
http://localhost:3000/api/auth/callback/google. - Fix: In Google Cloud Console, add all redirect URIs:
- Dev:
http://localhost:3000/api/auth/callback/google - Prod:
https://yourdomain.com/api/auth/callback/google - Status: βΉοΈ Configuration requirement.
16. Frontend volume mounts override built assets in dev
- Where:
docker-compose.ymlβfrontendservice - Problem: Volume mounts (
./heritage_graph_ui:/app) override the built.nextdirectory. Anonymous volumes (/app/node_modules,/app/.next) are used to prevent this, but can cause stale cache issues. - Fix: In development, use
docker-compose up --buildto rebuild. Or remove volume mounts and rely on image rebuilds.
17. PostgreSQL init script only runs on first boot
- Where:
infra/postgres/init-scripts/01-init-databases.sh - Problem: Docker's
docker-entrypoint-initdb.dscripts only run when the data directory is empty (firstdocker-compose up). - Impact: If you need to re-run init scripts, you must delete the volume:
docker-compose down -v.
π Debugging Tips
Check Django settings being used
docker-compose exec backend python -c "from django.conf import settings; print(settings.SETTINGS_MODULE)"
Check if database is reachable
docker-compose exec backend python -c "
import django; django.setup()
from django.db import connection
connection.ensure_connection()
print('DB OK')
"
Verify Google ID token
# Decode a Google ID token (for debugging)
python3 -c "import jwt; print(jwt.decode('YOUR_TOKEN', options={'verify_signature': False}))"
Check Traefik routing
curl -H "Host: backend.localhost" http://localhost/health/
curl -H "Host: frontend.localhost" http://localhost
View all Traefik routes
Open http://traefik.localhost:8080/dashboard/ in browser.
Run Django management commands
docker-compose exec backend python manage.py shell
docker-compose exec backend python manage.py showmigrations
docker-compose exec backend python manage.py check --deploy
Frontend build errors
# Check Next.js build output
docker-compose logs frontend | tail -50
# Rebuild with no cache
docker-compose build --no-cache frontend
π Checklist Before Deploying
- [ ]
.envfile created from.env.examplewith production values - [ ]
DJANGO_SECRET_KEYis randomly generated (not the default) - [ ]
POSTGRES_PASSWORDis a strong password - [ ]
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETare configured - [ ]
NEXTAUTH_SECRETis randomly generated - [ ]
DEBUG=Falsein.env - [ ]
ALLOWED_HOSTScontains your production domain - [ ]
NEXT_PUBLIC_API_URLpoints to production API URL - [ ] Google OAuth redirect URIs configured for production domain
- [ ] SSL/TLS is configured (Let's Encrypt or custom certs)
- [ ] Firewall allows only ports 80 and 443
- [ ] Database backups are scheduled
- [ ] Log rotation is configured
- [ ] Health check endpoints are accessible