@tyroneross/build-loop 0.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Lessons from Example Web App (2025 Build)
|
|
4
|
+
|
|
5
|
+
Real failures from shipping a Next.js + Supabase + Google OAuth app. Each one cost at least half a day. Read this when something breaks — there's a decent chance it's listed here.
|
|
6
|
+
|
|
7
|
+
## 1. "Works in dev, fails in prod" Auth — Supabase SSR Cookie API
|
|
8
|
+
|
|
9
|
+
**Symptom**: Sign-in works locally. In production, after Google consent the user lands back on `/signin`. No error in the UI. Vercel logs show:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
TypeError: Cannot read properties of undefined (reading 'get')
|
|
13
|
+
at <supabase ssr internals>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Root cause**: Supabase SSR ≥ 0.3 moved from `{ get, set, remove }` cookie methods to `{ getAll, setAll }`. Next.js 15 dev had fallback compat that masked the bug. Prod enforced the new contract.
|
|
17
|
+
|
|
18
|
+
**Fix**: Use `await cookies()` and `getAll`/`setAll` everywhere. Full recipe in `oauth-setup.md`.
|
|
19
|
+
|
|
20
|
+
**Prevention**: Test the prod build locally (`npm run build && npm start`) before every deploy. Dev-only bugs hide in the hot-reload fallback paths.
|
|
21
|
+
|
|
22
|
+
## 2. Route-Group Conflict → Vercel `ENOENT` manifest failure
|
|
23
|
+
|
|
24
|
+
**Symptom**: Local build works. Vercel build fails during "Collecting build traces":
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Error: ENOENT: no such file or directory,
|
|
28
|
+
lstat '/vercel/path0/.next/server/app/(app)/page_client-reference-manifest.js'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Root cause**: `app/page.tsx` AND `app/(app)/page.tsx` both exist. Next.js generates the manifest at `.next/server/app/page_client-reference-manifest.js`, but the tracer expects it at `.next/server/app/(app)/page_client-reference-manifest.js`. The app won't deploy until one of the pages is removed.
|
|
32
|
+
|
|
33
|
+
**Fix**:
|
|
34
|
+
```bash
|
|
35
|
+
rm app/page.tsx # if using app/(app)/page.tsx as the real root
|
|
36
|
+
npm run build
|
|
37
|
+
ls .next/server/app/(app)/*manifest* # verify manifest now exists
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Prevention**: Pick one root-level routing strategy up front — either a root `page.tsx` or a route group for authenticated views. Not both.
|
|
41
|
+
|
|
42
|
+
## 3. "100% Complete" Claims Without End-to-End Testing
|
|
43
|
+
|
|
44
|
+
**Symptom**: Status doc said "Stage 1: OAuth Authentication — 100% Complete". Actual state: sign-in was broken. This wasn't a technical bug — it was a process bug. Completion was claimed on "implementation written", not "flow works".
|
|
45
|
+
|
|
46
|
+
**Fix**: Never mark auth "done" until:
|
|
47
|
+
1. `npm run build` succeeds
|
|
48
|
+
2. You can sign in via Google in the prod build
|
|
49
|
+
3. Session survives hard refresh
|
|
50
|
+
4. Session survives browser close/reopen
|
|
51
|
+
5. Sign-out actually clears cookies
|
|
52
|
+
6. A protected API route returns 401 when unauthenticated
|
|
53
|
+
|
|
54
|
+
All six. Every time. See the verification checklist in `oauth-setup.md`.
|
|
55
|
+
|
|
56
|
+
## 4. Production Trace Logs Are Essential
|
|
57
|
+
|
|
58
|
+
**Observation**: Local debugging couldn't reproduce the Supabase SSR bug (#1). Only Vercel's production traces showed the exact error. Without them, debugging would have been blind guesses.
|
|
59
|
+
|
|
60
|
+
**Prevention**:
|
|
61
|
+
- Enable Vercel Log Drains or Sentry from day one
|
|
62
|
+
- Add correlation IDs to auth callback logs (`callback_id=abc` so you can trace one user's flow)
|
|
63
|
+
- Log the auth step at each stage: `auth.redirect`, `auth.callback_received`, `auth.tokens_exchanged`, `auth.session_set`
|
|
64
|
+
|
|
65
|
+
Don't wait for production issues to add observability.
|
|
66
|
+
|
|
67
|
+
## 5. `redirect_uri_mismatch` Chasing
|
|
68
|
+
|
|
69
|
+
**Symptom**: After clicking "Sign in with Google", lands on a Google error page: `Error 400: redirect_uri_mismatch`.
|
|
70
|
+
|
|
71
|
+
**Root cause — any of**:
|
|
72
|
+
- Trailing slash difference (`http://localhost:3000` vs `http://localhost:3000/`)
|
|
73
|
+
- Protocol difference (`http` vs `https`)
|
|
74
|
+
- Wrong port
|
|
75
|
+
- Using app URL as Google redirect when Supabase is in the middle (Supabase uses its own callback)
|
|
76
|
+
- Stale entry in Google Cloud Console that doesn't match current env var
|
|
77
|
+
|
|
78
|
+
**Fix**: The redirect URI you register in Google Cloud Console MUST match the one your app passes in the OAuth request **character-for-character**.
|
|
79
|
+
|
|
80
|
+
For Supabase:
|
|
81
|
+
```
|
|
82
|
+
https://<project-ref>.supabase.co/auth/v1/callback
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For direct OAuth:
|
|
86
|
+
```
|
|
87
|
+
https://your-domain.com/api/auth/callback/google
|
|
88
|
+
http://localhost:3000/api/auth/callback/google
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Prevention**: Add a boot-time check that logs the exact redirect URI your app will send, and compare it manually with the Cloud Console list during setup.
|
|
92
|
+
|
|
93
|
+
## 6. Credential Rotation Is Painful — Plan For It
|
|
94
|
+
|
|
95
|
+
**Observation**: When a secret leaked, rotating the Google client secret required:
|
|
96
|
+
1. Generate new secret in Google Cloud Console
|
|
97
|
+
2. Update Supabase Dashboard → Providers → Google
|
|
98
|
+
3. Update Vercel env var (`GOOGLE_CLIENT_SECRET`)
|
|
99
|
+
4. Trigger redeploy
|
|
100
|
+
5. Test sign-in flow
|
|
101
|
+
6. After verifying, delete old secret in Cloud Console
|
|
102
|
+
|
|
103
|
+
Steps 1-3 can be done with the old secret still active (Google supports two live secrets during rotation). If you skip the "both live" window, users in mid-session get `invalid_client`.
|
|
104
|
+
|
|
105
|
+
**Prevention**: Document the rotation procedure BEFORE you need it. Store credentials in a secret manager (Vercel Env, Doppler, AWS Secrets Manager) so rotation is one place to change.
|
|
106
|
+
|
|
107
|
+
## 7. "Demo" / "Placeholder" Values Shipped to Prod
|
|
108
|
+
|
|
109
|
+
**Symptom**: `.env.example` had `GOOGLE_CLIENT_ID=your_client_id_here`. Someone copied it to `.env.local`, it got committed, CI pushed it to prod. Sign-in broke silently.
|
|
110
|
+
|
|
111
|
+
**Fix**: Boot-time env validator that REFUSES to start in prod if any env var matches `your_|demo_|development|placeholder`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const bad = Object.entries(requiredVars).filter(([, v]) =>
|
|
115
|
+
!v || /^(your_|demo_|development|placeholder)/i.test(v)
|
|
116
|
+
)
|
|
117
|
+
if (bad.length && process.env.NODE_ENV === 'production') {
|
|
118
|
+
throw new Error(`Missing env vars: ${bad.map(([k]) => k).join(', ')}`)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 8. CSRF Defense — State Parameter
|
|
123
|
+
|
|
124
|
+
**Observation**: Without a state parameter, nothing stops a malicious site from forging an OAuth callback. Supabase's flow handles this internally; direct OAuth flows must generate + verify state themselves.
|
|
125
|
+
|
|
126
|
+
**Pattern**:
|
|
127
|
+
1. Generate a random state token, store in httpOnly cookie with 10-min TTL
|
|
128
|
+
2. Pass state in the authorize URL
|
|
129
|
+
3. On callback, compare returned state to the cookie
|
|
130
|
+
4. Mismatch → reject, redirect to signin with `error=state_mismatch`
|
|
131
|
+
|
|
132
|
+
See the code in `oauth-setup.md` section 6.
|
|
133
|
+
|
|
134
|
+
## 9. Session Refresh Needs a Threshold
|
|
135
|
+
|
|
136
|
+
**Symptom**: User's session expires mid-action, they lose unsaved work.
|
|
137
|
+
|
|
138
|
+
**Fix**: Check expiry proactively. Refresh if `expires_at - now < 15 minutes`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const timeUntilExpiry = session.expires_at - Math.floor(Date.now() / 1000)
|
|
142
|
+
if (timeUntilExpiry < 15 * 60) {
|
|
143
|
+
await supabase.auth.refreshSession()
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run this check on a 1-hour interval + on visibility change (user returns to tab).
|
|
148
|
+
|
|
149
|
+
## 10. Open Redirect via `returnTo`
|
|
150
|
+
|
|
151
|
+
**Symptom**: Code review flagged `?returnTo=http://evil.com` — sign-in flow would redirect to attacker's site.
|
|
152
|
+
|
|
153
|
+
**Fix**: Allow-list validation on the redirect URL:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
function isValidRedirect(url: string): boolean {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(url, process.env.NEXT_PUBLIC_APP_URL)
|
|
159
|
+
const allowed = ['localhost', '127.0.0.1', new URL(process.env.NEXT_PUBLIC_APP_URL!).hostname]
|
|
160
|
+
return allowed.includes(parsed.hostname)
|
|
161
|
+
} catch {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Never trust a query param as a navigation target without validating its host.
|
|
168
|
+
|
|
169
|
+
## Process Lessons (Not Technical)
|
|
170
|
+
|
|
171
|
+
These aren't bugs, they're habits that would have prevented half the bugs:
|
|
172
|
+
|
|
173
|
+
1. **Ship a sign-in E2E test before any other feature**. It's your canary. If sign-in breaks, everything else does.
|
|
174
|
+
2. **Separate dev and prod Google Cloud projects**, not just credentials. Quota and consent-screen state are project-scoped.
|
|
175
|
+
3. **Publish the OAuth consent screen early** so you hit Google verification before you have a deadline.
|
|
176
|
+
4. **Use certainty markers** in status docs (✅ verified / ⚠️ untested / ❓ unknown) — not "100% complete".
|
|
177
|
+
5. **Document rotation, scope changes, and env-var lifecycle** in a `docs/AUTH.md` — not buried in commits.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Google Maps JavaScript API — 2026 Modern Pattern
|
|
4
|
+
|
|
5
|
+
## Enable and Restrict
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Google Cloud Console → APIs & Services → Library
|
|
9
|
+
Enable: Maps JavaScript API
|
|
10
|
+
Enable: Places API (New) ← for autocomplete/search
|
|
11
|
+
Enable: Geocoding API ← for address ↔ lat/lng
|
|
12
|
+
Enable: Directions API ← only if you need routing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then:
|
|
16
|
+
```
|
|
17
|
+
APIs & Services → Credentials → + CREATE CREDENTIALS → API key
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Immediately restrict it** — unrestricted keys are scraped and billed within hours.
|
|
21
|
+
|
|
22
|
+
- **Application restrictions**: HTTP referrers (web sites)
|
|
23
|
+
- `https://your-domain.com/*`
|
|
24
|
+
- `http://localhost:3000/*` (for dev)
|
|
25
|
+
- **API restrictions**: Restrict key → select only the APIs you enabled above
|
|
26
|
+
|
|
27
|
+
Create **one key per environment** (dev, staging, prod). Use the restriction to prevent a leaked dev key from calling prod APIs.
|
|
28
|
+
|
|
29
|
+
Maps API keys are **public by design** — the browser sees them. Restriction, not secrecy, is what protects them.
|
|
30
|
+
|
|
31
|
+
## Get a Map ID (Required for Advanced Markers)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Google Cloud Console → Google Maps Platform → Map Management → CREATE MAP ID
|
|
35
|
+
Map type: JavaScript
|
|
36
|
+
Raster or Vector: Vector (for cloud-based styling + Advanced Markers)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Save the Map ID — without it, `AdvancedMarkerElement` silently refuses to render.
|
|
40
|
+
|
|
41
|
+
## Modern Async Loader Pattern
|
|
42
|
+
|
|
43
|
+
Don't use `<script src="https://maps.googleapis.com/maps/api/js?key=...">` anymore. Use the inline bootstrap loader with `importLibrary()`.
|
|
44
|
+
|
|
45
|
+
### Option A: Inline bootstrap (recommended — no npm dep)
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<script>
|
|
49
|
+
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
|
|
50
|
+
({key: "YOUR_API_KEY", v: "weekly"});
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Paste that once in your root layout. It exposes `google.maps.importLibrary()` globally without actually fetching the API until you call it.
|
|
55
|
+
|
|
56
|
+
### Option B: `@googlemaps/js-api-loader` (npm, typed)
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Loader } from '@googlemaps/js-api-loader'
|
|
60
|
+
|
|
61
|
+
const loader = new Loader({
|
|
62
|
+
apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
|
|
63
|
+
version: 'weekly',
|
|
64
|
+
libraries: ['maps', 'marker', 'places'],
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Per this repo's user preference (minimal deps, build from scratch), prefer **Option A** unless you already have the package for other reasons.
|
|
69
|
+
|
|
70
|
+
## Render a Map with Advanced Markers
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
'use client'
|
|
74
|
+
import { useEffect, useRef } from 'react'
|
|
75
|
+
|
|
76
|
+
declare global {
|
|
77
|
+
interface Window {
|
|
78
|
+
google: typeof google
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function CampMap({ camps }: { camps: { id: string; lat: number; lng: number; name: string }[] }) {
|
|
83
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let cancelled = false
|
|
87
|
+
async function init() {
|
|
88
|
+
if (!ref.current) return
|
|
89
|
+
const { Map } = await google.maps.importLibrary('maps') as google.maps.MapsLibrary
|
|
90
|
+
const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary('marker') as google.maps.MarkerLibrary
|
|
91
|
+
|
|
92
|
+
if (cancelled) return
|
|
93
|
+
|
|
94
|
+
const map = new Map(ref.current, {
|
|
95
|
+
center: { lat: 40.7128, lng: -74.006 },
|
|
96
|
+
zoom: 11,
|
|
97
|
+
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID!, // REQUIRED
|
|
98
|
+
disableDefaultUI: false,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
for (const camp of camps) {
|
|
102
|
+
const pin = new PinElement({
|
|
103
|
+
background: '#4F46E5',
|
|
104
|
+
borderColor: '#312E81',
|
|
105
|
+
glyphColor: '#FFF',
|
|
106
|
+
})
|
|
107
|
+
new AdvancedMarkerElement({
|
|
108
|
+
map,
|
|
109
|
+
position: { lat: camp.lat, lng: camp.lng },
|
|
110
|
+
title: camp.name,
|
|
111
|
+
content: pin.element,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
init()
|
|
116
|
+
return () => { cancelled = true }
|
|
117
|
+
}, [camps])
|
|
118
|
+
|
|
119
|
+
return <div ref={ref} className="h-[500px] w-full rounded-lg" />
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Deprecated — DO NOT USE
|
|
124
|
+
|
|
125
|
+
- `new google.maps.Marker(...)` — deprecated Feb 2024. Use `AdvancedMarkerElement`.
|
|
126
|
+
- `google.maps.event.addListener(marker, 'click', ...)` on old Markers — use DOM events on the advanced marker's `content` element or its `.addListener('gmp-click', ...)`.
|
|
127
|
+
- Synchronous script tag with `&callback=initMap` — still works but no IntelliSense/types and no lazy loading.
|
|
128
|
+
|
|
129
|
+
## Types
|
|
130
|
+
|
|
131
|
+
Install `@types/google.maps` (tiny, just ambient types):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm i -D @types/google.maps
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or hand-roll the needed types to avoid the dep. The ambient types are not pulled into the bundle.
|
|
138
|
+
|
|
139
|
+
## Common Failures
|
|
140
|
+
|
|
141
|
+
- **"Google Maps JavaScript API error: InvalidKeyMapError"** — key doesn't exist, isn't enabled on this project, or the Maps JS API isn't enabled.
|
|
142
|
+
- **"Google Maps JavaScript API error: ApiNotActivatedMapError"** — enable Maps JavaScript API on this project.
|
|
143
|
+
- **"Google Maps JavaScript API error: RefererNotAllowedMapError"** — current origin not in the key's HTTP referrer allow-list. Common during local dev if you only whitelisted prod.
|
|
144
|
+
- **Map renders but markers don't** — `mapId` missing, or you used `google.maps.Marker` without importing the `marker` library.
|
|
145
|
+
- **Blank map / grey square** — container has height 0. `h-[500px]` or equivalent must resolve to pixels.
|
|
146
|
+
- **"For development purposes only" watermark** — billing not enabled on the project. Google requires a billing account even for free-tier usage.
|
|
147
|
+
|
|
148
|
+
## Cost Control
|
|
149
|
+
|
|
150
|
+
Maps loads are $7/1000 after the free tier. Places API (New) Text Search is $32/1000 (without field mask). Aggressive field masks and client-side caching are critical.
|
|
151
|
+
|
|
152
|
+
- Cache place details in your DB (with TTL per Google's usage terms — 30 days for most fields)
|
|
153
|
+
- Use `fields` / `X-Goog-FieldMask` on every Places call
|
|
154
|
+
- Prefer Autocomplete (cheaper) over Text Search when the user is typing
|
|
155
|
+
- Session tokens on Autocomplete make a sequence of requests + one Place Details count as a single session (much cheaper)
|
|
156
|
+
|
|
157
|
+
## Sources
|
|
158
|
+
|
|
159
|
+
- [Load Maps JS API](https://developers.google.com/maps/documentation/javascript/load-maps-js-api)
|
|
160
|
+
- [Advanced Markers migration](https://developers.google.com/maps/documentation/javascript/advanced-markers/migration)
|
|
161
|
+
- [Advanced Markers reference](https://developers.google.com/maps/documentation/javascript/reference/advanced-markers)
|
|
162
|
+
- [Libraries](https://developers.google.com/maps/documentation/javascript/libraries)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# OAuth Setup — Console + App Wiring
|
|
4
|
+
|
|
5
|
+
End-to-end recipe for "Sign in with Google" on a web app. Includes Supabase flow (recommended for Next.js) and direct-OAuth flow (for when you're not using an auth provider).
|
|
6
|
+
|
|
7
|
+
## 1. Create the Google Cloud Project
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
console.cloud.google.com → project selector → NEW PROJECT
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use **separate projects for dev and prod**. Not just separate credentials — separate projects. This isolates quotas, billing, and consent-screen state.
|
|
14
|
+
|
|
15
|
+
## 2. Configure OAuth Consent Screen (DO THIS FIRST)
|
|
16
|
+
|
|
17
|
+
You can't create an OAuth client until the consent screen exists.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
APIs & Services → OAuth consent screen
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **User type**: External (unless you're inside a Workspace org and only need internal users)
|
|
24
|
+
- **App name**: Your app's display name (shown on consent dialog — users see this)
|
|
25
|
+
- **User support email**: Must be reachable
|
|
26
|
+
- **App domain / privacy policy / terms of service**: Required before you can move out of "Testing" mode
|
|
27
|
+
- **Authorized domains**: Root of your app URL (e.g. `your-domain.com` — no protocol)
|
|
28
|
+
- **Developer contact**: Your email
|
|
29
|
+
|
|
30
|
+
**Scopes**: Add only what you need. For basic sign-in:
|
|
31
|
+
- `openid`
|
|
32
|
+
- `https://www.googleapis.com/auth/userinfo.email`
|
|
33
|
+
- `https://www.googleapis.com/auth/userinfo.profile`
|
|
34
|
+
|
|
35
|
+
For Calendar read:
|
|
36
|
+
- `https://www.googleapis.com/auth/calendar.readonly`
|
|
37
|
+
|
|
38
|
+
For Calendar read+write:
|
|
39
|
+
- `https://www.googleapis.com/auth/calendar.events` (recommended — per-event access, narrower than `calendar`)
|
|
40
|
+
|
|
41
|
+
**Test users**: While in Testing mode, add every email that needs to sign in. Testing mode is fine for dev; publish before prod.
|
|
42
|
+
|
|
43
|
+
**Verification**: If you request any "sensitive" or "restricted" scopes (Calendar is sensitive), publishing the app triggers Google verification. Budget a week for first-time verification. Non-sensitive scopes (profile, email) don't need it.
|
|
44
|
+
|
|
45
|
+
## 3. Create the OAuth Web Client
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
APIs & Services → Credentials → + CREATE CREDENTIALS → OAuth client ID
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Application type**: Web application
|
|
52
|
+
- **Name**: `<app> Web Client (prod)` — name it so you can tell dev from prod later
|
|
53
|
+
- **Authorized JavaScript origins**:
|
|
54
|
+
- `https://your-domain.com`
|
|
55
|
+
- Local dev: `http://localhost:3000` (use the actual port)
|
|
56
|
+
- **Authorized redirect URIs** — EXACT MATCH, no trailing slash:
|
|
57
|
+
- **If using Supabase**: `https://<your-supabase-project>.supabase.co/auth/v1/callback` (NOT your app URL — Supabase handles the exchange)
|
|
58
|
+
- **If direct OAuth**: `https://your-domain.com/api/auth/callback/google`
|
|
59
|
+
- Local dev: `http://localhost:3000/api/auth/callback/google`
|
|
60
|
+
|
|
61
|
+
Copy the **Client ID** and **Client Secret** immediately. The secret is visible only until you navigate away; you can re-download later but it's easier to store now. Treat the secret like a password.
|
|
62
|
+
|
|
63
|
+
## 4. Wire Up Supabase (Recommended for Next.js Apps)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Supabase Dashboard → Authentication → Providers → Google → ON
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Paste **Client ID** and **Client Secret**
|
|
70
|
+
- Save
|
|
71
|
+
- Then: `Authentication → URL Configuration`
|
|
72
|
+
- **Site URL**: `https://your-domain.com` (must match `NEXT_PUBLIC_APP_URL` in your app)
|
|
73
|
+
- **Redirect URLs**: `https://your-domain.com/**` and `http://localhost:3000/**`
|
|
74
|
+
|
|
75
|
+
Supabase handles the token exchange, session cookies, and refresh-token storage. You don't write any OAuth code — you call `supabase.auth.signInWithOAuth({ provider: 'google' })`.
|
|
76
|
+
|
|
77
|
+
## 5. Next.js 16 Cookie API (CRITICAL)
|
|
78
|
+
|
|
79
|
+
This is the #1 production auth bug. Next.js 15+ made `cookies()` async; Supabase SSR ≥ 0.3 requires the `getAll`/`setAll` shape, not the old `get`/`set`/`remove`.
|
|
80
|
+
|
|
81
|
+
### ❌ OLD — DO NOT USE
|
|
82
|
+
```ts
|
|
83
|
+
// This fails in production on Next.js 15+
|
|
84
|
+
const cookieStore = cookies()
|
|
85
|
+
createServerClient(url, key, {
|
|
86
|
+
cookies: {
|
|
87
|
+
get: cookieStore.get,
|
|
88
|
+
set: cookieStore.set,
|
|
89
|
+
remove: cookieStore.remove,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### ✅ NEW — REQUIRED
|
|
95
|
+
```ts
|
|
96
|
+
// lib/supabase/server.ts
|
|
97
|
+
import { createServerClient } from '@supabase/ssr'
|
|
98
|
+
import { cookies } from 'next/headers'
|
|
99
|
+
import type { Database } from '@/types/database'
|
|
100
|
+
|
|
101
|
+
export async function createServerSupabase() {
|
|
102
|
+
const cookieStore = await cookies() // NOTE: await
|
|
103
|
+
return createServerClient<Database>(
|
|
104
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
105
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
106
|
+
{
|
|
107
|
+
cookies: {
|
|
108
|
+
getAll() {
|
|
109
|
+
return cookieStore.getAll()
|
|
110
|
+
},
|
|
111
|
+
setAll(cookiesToSet) {
|
|
112
|
+
try {
|
|
113
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
114
|
+
cookieStore.set(name, value, options)
|
|
115
|
+
)
|
|
116
|
+
} catch {
|
|
117
|
+
// Called from a Server Component — set via middleware instead
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Why the try/catch?
|
|
127
|
+
|
|
128
|
+
Supabase SSR tries to `set` cookies during Server Component rendering, but Next.js disallows that (Server Components can't set cookies). The try/catch lets it fail silently; the middleware refresh below handles the actual cookie writes.
|
|
129
|
+
|
|
130
|
+
### Middleware session refresh
|
|
131
|
+
```ts
|
|
132
|
+
// middleware.ts
|
|
133
|
+
import { createServerClient } from '@supabase/ssr'
|
|
134
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
135
|
+
|
|
136
|
+
export async function middleware(request: NextRequest) {
|
|
137
|
+
let response = NextResponse.next({ request })
|
|
138
|
+
|
|
139
|
+
const supabase = createServerClient(
|
|
140
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
141
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
142
|
+
{
|
|
143
|
+
cookies: {
|
|
144
|
+
getAll() { return request.cookies.getAll() },
|
|
145
|
+
setAll(cookiesToSet) {
|
|
146
|
+
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
|
147
|
+
response = NextResponse.next({ request })
|
|
148
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
149
|
+
response.cookies.set(name, value, options)
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Must call getUser to refresh the session
|
|
157
|
+
await supabase.auth.getUser()
|
|
158
|
+
return response
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const config = {
|
|
162
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 6. Direct OAuth Flow (No Supabase)
|
|
167
|
+
|
|
168
|
+
Use this only if you're not using Supabase/Auth.js/Clerk. Most of the time, one of those is the right call.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// app/api/auth/google/route.ts
|
|
172
|
+
import { NextResponse } from 'next/server'
|
|
173
|
+
import { randomBytes } from 'crypto'
|
|
174
|
+
import { cookies } from 'next/headers'
|
|
175
|
+
|
|
176
|
+
export async function GET() {
|
|
177
|
+
const state = randomBytes(32).toString('hex')
|
|
178
|
+
const cookieStore = await cookies()
|
|
179
|
+
|
|
180
|
+
// Store state in httpOnly cookie for CSRF validation
|
|
181
|
+
cookieStore.set('oauth_state', state, {
|
|
182
|
+
httpOnly: true,
|
|
183
|
+
secure: process.env.NODE_ENV === 'production',
|
|
184
|
+
sameSite: 'lax',
|
|
185
|
+
maxAge: 600, // 10 min
|
|
186
|
+
path: '/',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const params = new URLSearchParams({
|
|
190
|
+
client_id: process.env.GOOGLE_CLIENT_ID!,
|
|
191
|
+
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/google`,
|
|
192
|
+
response_type: 'code',
|
|
193
|
+
scope: 'openid email profile',
|
|
194
|
+
state,
|
|
195
|
+
access_type: 'offline', // get refresh token
|
|
196
|
+
prompt: 'consent', // force refresh token on every consent
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return NextResponse.redirect(
|
|
200
|
+
`https://accounts.google.com/o/oauth2/v2/auth?${params}`
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// app/api/auth/callback/google/route.ts
|
|
207
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
208
|
+
import { cookies } from 'next/headers'
|
|
209
|
+
|
|
210
|
+
export async function GET(request: NextRequest) {
|
|
211
|
+
const url = new URL(request.url)
|
|
212
|
+
const code = url.searchParams.get('code')
|
|
213
|
+
const state = url.searchParams.get('state')
|
|
214
|
+
const cookieStore = await cookies()
|
|
215
|
+
const savedState = cookieStore.get('oauth_state')?.value
|
|
216
|
+
|
|
217
|
+
// CSRF check
|
|
218
|
+
if (!code) return NextResponse.redirect('/signin?error=missing_code')
|
|
219
|
+
if (!state || !savedState) return NextResponse.redirect('/signin?error=state_missing')
|
|
220
|
+
if (state !== savedState) return NextResponse.redirect('/signin?error=state_mismatch')
|
|
221
|
+
|
|
222
|
+
// Exchange code for tokens
|
|
223
|
+
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
226
|
+
body: new URLSearchParams({
|
|
227
|
+
code,
|
|
228
|
+
client_id: process.env.GOOGLE_CLIENT_ID!,
|
|
229
|
+
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
230
|
+
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/google`,
|
|
231
|
+
grant_type: 'authorization_code',
|
|
232
|
+
}),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
if (!tokenRes.ok) return NextResponse.redirect('/signin?error=oauth_exchange_error')
|
|
236
|
+
|
|
237
|
+
const tokens = await tokenRes.json()
|
|
238
|
+
// tokens: { access_token, refresh_token, expires_in, id_token, scope, token_type }
|
|
239
|
+
|
|
240
|
+
// Store refresh_token securely server-side (e.g. encrypted DB column)
|
|
241
|
+
// Store a session cookie for the user
|
|
242
|
+
|
|
243
|
+
cookieStore.delete('oauth_state')
|
|
244
|
+
return NextResponse.redirect('/dashboard')
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## 7. Env Validation
|
|
249
|
+
|
|
250
|
+
Refuse to run with missing or placeholder values. Add this to `instrumentation.ts` so it runs at boot.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// instrumentation.ts
|
|
254
|
+
export function register() {
|
|
255
|
+
const required = {
|
|
256
|
+
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
257
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
|
258
|
+
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
|
259
|
+
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
|
260
|
+
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const bad = Object.entries(required).filter(([, v]) =>
|
|
264
|
+
!v || /^(your_|demo_|development|placeholder)/i.test(v)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if (bad.length > 0) {
|
|
268
|
+
const names = bad.map(([k]) => k).join(', ')
|
|
269
|
+
if (process.env.NODE_ENV === 'production') {
|
|
270
|
+
throw new Error(`Missing/placeholder env vars: ${names}`)
|
|
271
|
+
}
|
|
272
|
+
console.warn(`⚠️ Auth config incomplete: ${names}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Prod sanity
|
|
276
|
+
if (process.env.NODE_ENV === 'production') {
|
|
277
|
+
if (!process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://')) {
|
|
278
|
+
throw new Error('NEXT_PUBLIC_APP_URL must use HTTPS in production')
|
|
279
|
+
}
|
|
280
|
+
if (process.env.NEXT_PUBLIC_APP_URL !== process.env.NEXT_PUBLIC_SITE_URL) {
|
|
281
|
+
throw new Error('APP_URL and SITE_URL must match in production')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 8. Redirect URL Validation
|
|
288
|
+
|
|
289
|
+
Never blindly trust a `returnTo` query param — it's the classic open-redirect vector.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
export function isValidRedirectUrl(url: string | null): boolean {
|
|
293
|
+
if (!url) return false
|
|
294
|
+
try {
|
|
295
|
+
const parsed = new URL(url, process.env.NEXT_PUBLIC_APP_URL)
|
|
296
|
+
const allowed = [
|
|
297
|
+
'localhost',
|
|
298
|
+
'127.0.0.1',
|
|
299
|
+
new URL(process.env.NEXT_PUBLIC_APP_URL!).hostname,
|
|
300
|
+
]
|
|
301
|
+
return allowed.includes(parsed.hostname)
|
|
302
|
+
} catch {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## 9. Error Code Mapping
|
|
309
|
+
|
|
310
|
+
Give users actionable messages. Don't expose raw Google/Supabase errors.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
export function oauthErrorMessage(code: string): string {
|
|
314
|
+
const map: Record<string, string> = {
|
|
315
|
+
missing_code: 'Authorization failed. Please try signing in again.',
|
|
316
|
+
state_missing: 'Security validation failed. Please try again.',
|
|
317
|
+
state_mismatch: 'Security validation failed. Please try again.',
|
|
318
|
+
session_failed: 'Failed to create session. Please try again.',
|
|
319
|
+
oauth_exchange_error: 'OAuth token exchange failed. Please try again.',
|
|
320
|
+
access_denied: 'You denied access. Please grant permission to sign in.',
|
|
321
|
+
invalid_grant: 'Authorization expired. Please sign in again.',
|
|
322
|
+
invalid_client: 'Configuration error. Please contact support.',
|
|
323
|
+
browser_extension_conflict: 'Browser extension is blocking sign-in. Disable ad blockers and retry.',
|
|
324
|
+
nextjs_cookie_error: 'Cookies are disabled. Enable cookies and retry.',
|
|
325
|
+
}
|
|
326
|
+
return map[code] ?? 'Authentication failed. Please try again.'
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 10. Verification Checklist
|
|
331
|
+
|
|
332
|
+
Don't mark auth "done" until all of these pass:
|
|
333
|
+
|
|
334
|
+
- [ ] `npm run build` succeeds
|
|
335
|
+
- [ ] Sign-in button navigates to Google consent screen
|
|
336
|
+
- [ ] Consent grants → redirect to callback → lands on dashboard
|
|
337
|
+
- [ ] Hard refresh → still signed in
|
|
338
|
+
- [ ] Close browser, reopen → still signed in
|
|
339
|
+
- [ ] Sign-out clears cookies and redirects to `/signin`
|
|
340
|
+
- [ ] Protected route redirects unauthenticated users
|
|
341
|
+
- [ ] Protected API route returns 401 for unauthenticated requests
|
|
342
|
+
- [ ] Works in prod build locally (`npm run build && npm start`)
|
|
343
|
+
- [ ] Works in actual production deploy (NOT just dev)
|