Skip to content

feat(analytics): integrate PostHog for user analytics and consent management#74

Merged
JoachimLK merged 12 commits intomainfrom
feat/posthog-integration
Mar 9, 2026
Merged

feat(analytics): integrate PostHog for user analytics and consent management#74
JoachimLK merged 12 commits intomainfrom
feat/posthog-integration

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Mar 9, 2026

  • Added PostHog Nuxt module and posthog-node for server-side event capture.
  • Implemented useAnalyticsConsent composable for managing user consent (GDPR compliance).
  • Created usePostHogIdentity composable to sync Better Auth session with PostHog identity.
  • Developed posthog-identity client plugin to identify users and organizations in PostHog.
  • Introduced server-side PostHog client for backend event capture.

Summary

  • What does this PR change?
  • Why is this needed?

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features

    • Switched site analytics to PostHog with consent-based controls (banner to accept/decline) that persist and apply immediately.
    • Authenticated users and active organization context are automatically tied to analytics; tracking is cleared on sign-out.
    • Analytics sanitizes URLs by stripping query strings and fragments.
    • Introduced a reusable top navigation component used across public pages.
  • Chores

    • Added PostHog support on client and server.
    • Added a published Privacy Policy document.

…agement

- Added PostHog Nuxt module and posthog-node for server-side event capture.
- Implemented useAnalyticsConsent composable for managing user consent (GDPR compliance).
- Created usePostHogIdentity composable to sync Better Auth session with PostHog identity.
- Developed posthog-identity client plugin to identify users and organizations in PostHog.
- Introduced server-side PostHog client for backend event capture.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds PostHog analytics integration: conditional module/config and deps, client plugin and composables for identity and consent, server PostHog client utility, DB URL resolver helpers, new ConsentBanner and PublicNavBar components, and a top-level await in root app to sync identity before rendering.

Changes

Cohort / File(s) Summary
Nuxt config & deps
nuxt.config.ts, package.json
Conditional inclusion of @posthog/nuxt; new posthogConfig and runtimeConfig.public.posthog; added @posthog/nuxt and posthog-node deps; Plausible script removed.
Client plugin (posthog identity)
app/plugins/posthog-identity.client.ts
New client plugin exposing posthogIdentifyUser, posthogSetOrganization, posthogReset, posthogResetGroups; sanitizes $current_url in captures; activation guarded by PostHog presence.
Client composables
app/composables/usePostHogIdentity.ts, app/composables/useAnalyticsConsent.ts
usePostHogIdentity() watches auth session and org to call provided PostHog helpers; useAnalyticsConsent() manages consent state in localStorage and calls PostHog opt-in/out accordingly.
App root & UI components
app/app.vue, app/components/ConsentBanner.vue, app/components/PublicNavBar.vue
Top-level await usePostHogIdentity() added in script setup; ConsentBanner added and wrapped with client-only rendering; PublicNavBar added and replaces inline navs across pages (prop activePage).
Pages updated to use nav
app/pages/blog/..., app/pages/blog/index.vue, app/pages/catalog/index.vue, app/pages/docs/index.vue, app/pages/roadmap.vue
Replaced manual nav markup with PublicNavBar and removed Github icon imports where applicable; minor icon/import adjustments.
Docs content
content/docs/6.legal/1.privacy-policy.md
New comprehensive privacy policy document (consent-based analytics, data handling, retention, rights, contact).
Server PostHog & DB URL helpers
server/utils/posthog.ts, drizzle.config.ts, server/scripts/seed.ts
New useServerPostHog() singleton initializer for server-side events; added resolveDatabaseUrl() helper in drizzle config and seed script to robustly construct DATABASE_URL.
Misc pages/icons
app/pages/docs/[...slug].vue
Added Scale icon and 'Legal' section mapping for docs sidebar.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User
    participant Browser as "Browser / Client"
    participant App as "Nuxt App"
    participant Auth as "Auth Session"
    participant Consent as "useAnalyticsConsent"
    participant Identity as "usePostHogIdentity"
    participant Plugin as "posthog-identity plugin"
    participant PostHog as "PostHog Service"
    participant Server as "Server (useServerPostHog)"

    User->>Browser: navigate to app
    Browser->>App: mount
    App->>Identity: top-level await usePostHogIdentity()
    Identity->>Auth: subscribe to session & org (immediate)
    Auth-->>Identity: session with user
    Identity->>Plugin: posthogIdentifyUser(user)
    Plugin->>PostHog: identify(user.id, {name, createdAt})
    Identity->>Plugin: posthogSetOrganization(org)
    Plugin->>PostHog: group('organization', org.id, {name, slug})
    App->>Consent: check consent (localStorage)
    Consent-->>Plugin: opt_in/opt_out (if PostHog present)
    Browser->>Plugin: capture(event) [query/hash stripped]
    Plugin->>PostHog: capture(event)
    App->>Server: request causing server event
    Server->>Server: useServerPostHog() -> cached client
    Server->>PostHog: capture(server event)
    Auth-->>Identity: user signed out
    Identity->>Plugin: posthogReset()
    Plugin->>PostHog: reset()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped in quick to tie a trace,

I nudged the session, set the place.
Consent tucked snug in local lore,
Client and server hum once more.
A tiny rabbit clap — events embrace.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes a summary of the key changes but lacks completed validation checkboxes and explicit answers to the template's 'What' and 'Why' questions. The template structure is present but largely unfilled. Clearly answer 'What does this PR change?' and 'Why is this needed?' sections, and check the relevant Type of change and Validation checkboxes to confirm local testing and documentation updates.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature: PostHog integration with analytics and consent management, which aligns with the primary changes across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/posthog-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
server/utils/posthog.ts (1)

12-31: Consider flushing events on server shutdown.

The singleton PostHog client buffers events (up to 20 events or 10 seconds). If the server shuts down gracefully, buffered events may be lost. PostHog's Node SDK provides a shutdown() method to flush pending events.

♻️ Proposed enhancement to handle graceful shutdown
 export function useServerPostHog(): PostHog | null {
   if (client) return client

   const config = useRuntimeConfig()
   const publicKey = config.public.posthog?.publicKey
   const host = config.public.posthog?.host

   if (!publicKey) {
     return null
   }

   client = new PostHog(publicKey, {
     host: host || 'https://eu.i.posthog.com',
     // Flush events every 10 seconds or 20 events, whichever comes first
     flushAt: 20,
     flushInterval: 10_000,
   })

+  // Flush pending events on graceful shutdown
+  process.on('SIGTERM', async () => {
+    await client?.shutdown()
+  })
+
   return client
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/posthog.ts` around lines 12 - 31, The PostHog singleton created
in useServerPostHog currently buffers events and never flushes on shutdown;
update useServerPostHog to register a graceful shutdown handler that calls
client.shutdown() (or awaits it) when the process/server is stopping so buffered
events are flushed; reference the existing client and PostHog instance returned
by useServerPostHog and ensure the shutdown handler is idempotent (only
registers once) and handles errors from client.shutdown() appropriately.
app/plugins/posthog-identity.client.ts (1)

16-31: Consider sanitizing $referrer as well.

The URL sanitization correctly strips query params and hashes from $current_url, but $referrer can also contain sensitive data (e.g., if the user came from a page with tokens in the URL).

♻️ Proposed enhancement to also sanitize $referrer
     posthog.capture = (eventName: string, properties?: Record<string, unknown>, options?: unknown) => {
       const props = { ...properties }
-      if (typeof props['$current_url'] === 'string') {
-        try {
-          const url = new URL(props['$current_url'])
-          url.search = ''
-          url.hash = ''
-          props['$current_url'] = url.toString()
-        }
-        catch { /* keep original if parsing fails */ }
-      }
+      for (const key of ['$current_url', '$referrer']) {
+        if (typeof props[key] === 'string') {
+          try {
+            const url = new URL(props[key])
+            url.search = ''
+            url.hash = ''
+            props[key] = url.toString()
+          }
+          catch { /* keep original if parsing fails */ }
+        }
+      }
       return originalCapture(eventName, props, options as never)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plugins/posthog-identity.client.ts` around lines 16 - 31, The
sanitization currently strips query and hash from props['$current_url'] but not
props['$referrer']; update posthog.capture to apply the same URL-parsing logic
to props['$referrer'] as well (use the same try/catch block used for
props['$current_url']), clearing search and hash on successful parse, leaving
the original value if parsing fails, and then call originalCapture(eventName,
props, options) as before.
app/app.vue (1)

11-13: Top-level await may delay initial render.

The await usePostHogIdentity() blocks rendering until the auth session is fetched. This is intentional to ensure identity sync before analytics events, but could impact perceived load time for users.

Consider whether the identity sync could be deferred to after initial render (e.g., in an onMounted hook or via a non-blocking pattern) if load performance becomes a concern in testing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/app.vue` around lines 11 - 13, The top-level await on usePostHogIdentity
is blocking initial render; change it to run post-mount or non-blocking: move
the call into the component lifecycle (e.g., call usePostHogIdentity() from
onMounted and await there) or invoke it without awaiting (fire-and-forget) so
rendering isn't delayed; locate the current top-level await usePostHogIdentity()
in app.vue and replace it with an onMounted(() => { await usePostHogIdentity();
}) or onMounted(() => { usePostHogIdentity(); }) depending on whether you need
to wait for completion before other post-mount work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useAnalyticsConsent.ts`:
- Around line 42-45: Change default PostHog behavior to opt-out-by-default and
ensure the composable enforces opt-out until explicit accept; set
opt_out_capturing_by_default: true in nuxt.config.ts and update the
useAnalyticsConsent logic so that on mount it always calls
posthog?.opt_out_capturing() unless consentState === 'granted', and only calls
posthog?.opt_in_capturing() (or equivalent) from acceptAnalytics(); reference
useAnalyticsConsent, consentState, acceptAnalytics, and the
posthog?.opt_out_capturing()/opt_in_capturing() calls when making these changes.
- Around line 14-54: The composable useAnalyticsConsent is never used in the app
so users cannot grant/deny analytics; implement a small ConsentBanner component
that imports useAnalyticsConsent and displays when needsConsent is true, calling
acceptAnalytics() and declineAnalytics() on the respective buttons and using
hasConsented/hasDeclined for state, then mount that component globally by adding
it to the root layout (e.g., default layout or App.vue) so it renders for all
pages; ensure the component only accesses localStorage/client-only behavior on
import.meta.client or inside onMounted to match the composable’s SSR checks and
ensure posthog methods (posthog?.opt_in_capturing/opt_out_capturing via the
composable) are invoked when buttons are clicked.

In `@app/composables/usePostHogIdentity.ts`:
- Around line 36-49: The watcher on activeOrgState currently only calls
$posthogSetOrganization when org?.id is truthy, leaving previous org groups set
when org becomes null; modify the callback in the watch (the function
referencing activeOrgState, org and $posthogSetOrganization) to call
resetGroups() (PostHog's resetGroups) when org is falsy/undefined so all
organization group associations are cleared, otherwise continue to call
$posthogSetOrganization with the id/name/slug as before; ensure you import or
reference resetGroups in the same scope as $posthogSetOrganization and keep the
immediate: true behavior.

---

Nitpick comments:
In `@app/app.vue`:
- Around line 11-13: The top-level await on usePostHogIdentity is blocking
initial render; change it to run post-mount or non-blocking: move the call into
the component lifecycle (e.g., call usePostHogIdentity() from onMounted and
await there) or invoke it without awaiting (fire-and-forget) so rendering isn't
delayed; locate the current top-level await usePostHogIdentity() in app.vue and
replace it with an onMounted(() => { await usePostHogIdentity(); }) or
onMounted(() => { usePostHogIdentity(); }) depending on whether you need to wait
for completion before other post-mount work.

In `@app/plugins/posthog-identity.client.ts`:
- Around line 16-31: The sanitization currently strips query and hash from
props['$current_url'] but not props['$referrer']; update posthog.capture to
apply the same URL-parsing logic to props['$referrer'] as well (use the same
try/catch block used for props['$current_url']), clearing search and hash on
successful parse, leaving the original value if parsing fails, and then call
originalCapture(eventName, props, options) as before.

In `@server/utils/posthog.ts`:
- Around line 12-31: The PostHog singleton created in useServerPostHog currently
buffers events and never flushes on shutdown; update useServerPostHog to
register a graceful shutdown handler that calls client.shutdown() (or awaits it)
when the process/server is stopping so buffered events are flushed; reference
the existing client and PostHog instance returned by useServerPostHog and ensure
the shutdown handler is idempotent (only registers once) and handles errors from
client.shutdown() appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74673f5f-27fd-4189-b3fa-b736872c54ca

📥 Commits

Reviewing files that changed from the base of the PR and between 021f8db and 619f239.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • app/app.vue
  • app/composables/useAnalyticsConsent.ts
  • app/composables/usePostHogIdentity.ts
  • app/plugins/posthog-identity.client.ts
  • nuxt.config.ts
  • package.json
  • server/utils/posthog.ts

Comment thread app/composables/useAnalyticsConsent.ts
Comment thread app/composables/useAnalyticsConsent.ts Outdated
Comment thread app/composables/usePostHogIdentity.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/composables/useAnalyticsConsent.ts (1)

46-49: Consider symmetrically applying opt_in_capturing for users who previously granted consent.

Currently, only opt_out_capturing() is called for 'denied' state on initialization. If a user previously granted consent but PostHog's internal state becomes out of sync (e.g., cookies cleared, library reset), the opt_in_capturing() call is never re-applied.

This is likely fine in practice since PostHog defaults to capturing enabled, but symmetric handling would be more defensive.

♻️ Optional: Apply consent symmetrically
   // Apply stored consent on mount
-  if (import.meta.client && consentState.value === 'denied') {
-    ph?.opt_out_capturing()
+  if (import.meta.client) {
+    if (consentState.value === 'denied') {
+      ph?.opt_out_capturing()
+    } else if (consentState.value === 'granted') {
+      ph?.opt_in_capturing()
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useAnalyticsConsent.ts` around lines 46 - 49, The
initialization only calls ph?.opt_out_capturing() when consentState.value ===
'denied', so add a symmetric call to ph?.opt_in_capturing() when
consentState.value indicates consent was granted (e.g., 'granted' or the
accepted enum/string your app uses); update the block in useAnalyticsConsent.ts
that currently checks import.meta.client and consentState.value === 'denied' to
also check for the granted state and call ph?.opt_in_capturing() accordingly,
ensuring you use the same consentState variable and the ph optional chain
already present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useAnalyticsConsent.ts`:
- Around line 21-24: The code unsafely casts localStorage value to ConsentState
in the useState initializer (consentState) which can produce invalid/ambiguous
states; instead, read the raw string from localStorage via CONSENT_KEY, validate
it against the allowed ConsentState values (e.g., "accepted" and "declined" or
whatever the ConsentState union contains) and only return a typed ConsentState
when it matches, otherwise return null (or the explicit default) so the app
shows the consent prompt; update the initializer in useAnalyticsConsent.ts to
perform this validation before assigning to consentState.

---

Nitpick comments:
In `@app/composables/useAnalyticsConsent.ts`:
- Around line 46-49: The initialization only calls ph?.opt_out_capturing() when
consentState.value === 'denied', so add a symmetric call to
ph?.opt_in_capturing() when consentState.value indicates consent was granted
(e.g., 'granted' or the accepted enum/string your app uses); update the block in
useAnalyticsConsent.ts that currently checks import.meta.client and
consentState.value === 'denied' to also check for the granted state and call
ph?.opt_in_capturing() accordingly, ensuring you use the same consentState
variable and the ph optional chain already present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8162a2dd-ed24-488b-9d3e-d35889f9d11a

📥 Commits

Reviewing files that changed from the base of the PR and between f532a3e and 1e948cb.

📒 Files selected for processing (3)
  • app/composables/useAnalyticsConsent.ts
  • app/plugins/posthog-identity.client.ts
  • nuxt.config.ts
✅ Files skipped from review due to trivial changes (1)
  • nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/plugins/posthog-identity.client.ts

Comment thread app/composables/useAnalyticsConsent.ts Outdated
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 16:26 Destroyed
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Mar 9, 2026

🚅 Deployed to the reqcore-pr-74 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Web Mar 9, 2026 at 6:05 pm

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 16:39 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/scripts/seed.ts (1)

42-67: Extract resolveDatabaseUrl() into a shared utility.

This is now a second copy of the resolver from drizzle.config.ts:8-34, and the two paths already diverge on failure handling (return '' here vs a detailed throw there). That makes future env fixes easy to apply in one path and miss in the other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/scripts/seed.ts` around lines 42 - 67, The resolveDatabaseUrl()
implementation is duplicated; extract it into a shared utility (e.g., export a
resolveDatabaseUrl function from a common module used by both drizzle.config.ts
and server/scripts/seed.ts), update seed.ts to import and call that shared
resolveDatabaseUrl instead of its local version, and remove the local copy;
ensure the shared function's failure behavior is the canonical one (make its
error handling/throwing consistent with the version in drizzle.config.ts) so
both DATABASE_URL resolution paths use the same logic and error reporting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle.config.ts`:
- Around line 3-41: drizzle.config.ts implements resolveDatabaseUrl() to fall
back to individual PG*/RAILWAY_TCP_PROXY_* vars; replicate that logic in runtime
by extracting or reusing resolveDatabaseUrl for application startup so env
validation and DB connections use the same fallback. Update server/utils/env.ts
to validate/process the DB URL via the same resolveDatabaseUrl (or a new shared
getDatabaseUrl helper) instead of only checking process.env.DATABASE_URL, and
update server/utils/db.ts to call that helper when constructing the DB client
(replace direct use of env.DATABASE_URL). Ensure the helper preserves
encoding/port/user/password behavior and throws the same descriptive error when
no host can be resolved so migrations and runtime behave identically.

---

Nitpick comments:
In `@server/scripts/seed.ts`:
- Around line 42-67: The resolveDatabaseUrl() implementation is duplicated;
extract it into a shared utility (e.g., export a resolveDatabaseUrl function
from a common module used by both drizzle.config.ts and server/scripts/seed.ts),
update seed.ts to import and call that shared resolveDatabaseUrl instead of its
local version, and remove the local copy; ensure the shared function's failure
behavior is the canonical one (make its error handling/throwing consistent with
the version in drizzle.config.ts) so both DATABASE_URL resolution paths use the
same logic and error reporting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c0eac53-05be-48f0-b3a0-189eedeac08b

📥 Commits

Reviewing files that changed from the base of the PR and between 1e948cb and 0302102.

📒 Files selected for processing (3)
  • drizzle.config.ts
  • nuxt.config.ts
  • server/scripts/seed.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • nuxt.config.ts

Comment thread drizzle.config.ts
Comment on lines +3 to +41
/**
* Resolve a valid database URL, with a fallback for Railway PR/preview environments
* where DATABASE_URL may have an empty hostname due to unresolved variable references.
* Falls back to individual PG* and RAILWAY_TCP_PROXY_* variables when available.
*/
function resolveDatabaseUrl(): string {
const raw = process.env.DATABASE_URL ?? ''

try {
const parsed = new URL(raw)
if (parsed.hostname) return raw
}
catch {
// fall through to individual-variable reconstruction
}

const host = process.env.PGHOST ?? process.env.RAILWAY_TCP_PROXY_DOMAIN ?? ''
const port = process.env.PGPORT ?? process.env.RAILWAY_TCP_PROXY_PORT ?? '5432'
const user = process.env.PGUSER ?? 'postgres'
const password = process.env.PGPASSWORD ?? ''
const database = process.env.PGDATABASE ?? 'railway'

if (host) {
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`
}

throw new Error(
`DATABASE_URL is missing a hostname and no PGHOST fallback is available.\n`
+ `Raw DATABASE_URL: "${raw}"\n`
+ `In Railway PR environments, ensure the Postgres service variables are linked to this service.`,
)
}

export default defineConfig({
schema: './server/database/schema/index.ts',
out: './server/database/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
url: resolveDatabaseUrl(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the same DB URL fallback in the runtime path.

drizzle.config.ts now handles Railway-style split PG vars, but server/utils/env.ts still validates only a raw DATABASE_URL, and server/utils/db.ts still connects with env.DATABASE_URL directly. In preview environments that means migrations can pass while the app still fails to boot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle.config.ts` around lines 3 - 41, drizzle.config.ts implements
resolveDatabaseUrl() to fall back to individual PG*/RAILWAY_TCP_PROXY_* vars;
replicate that logic in runtime by extracting or reusing resolveDatabaseUrl for
application startup so env validation and DB connections use the same fallback.
Update server/utils/env.ts to validate/process the DB URL via the same
resolveDatabaseUrl (or a new shared getDatabaseUrl helper) instead of only
checking process.env.DATABASE_URL, and update server/utils/db.ts to call that
helper when constructing the DB client (replace direct use of env.DATABASE_URL).
Ensure the helper preserves encoding/port/user/password behavior and throws the
same descriptive error when no host can be resolved so migrations and runtime
behave identically.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 17:05 Destroyed
…ent across blog, catalog, docs, and roadmap pages
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 17:16 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/roadmap.vue (1)

2-26: ⚠️ Potential issue | 🔴 Critical

Missing Github import causes build error.

The Github icon was removed from the imports but is still used at line 712 in the template (<Github class="h-3.5 w-3.5" />). This will cause a build/runtime error.

🐛 Proposed fix to restore import
 import {
   ArrowRight,
   Check,
   Hammer,
   Telescope,
   Star,
   ChevronRight,
   Layers,
   LogIn,
   Palette,
   Briefcase,
   Users,
   GitBranch,
   FileText,
   Globe,
   ClipboardList,
   LayoutDashboard,
   Search,
   ScanText,
   Sparkles,
   MessageSquare,
   Mail,
   Shield,
+  Github,
   type LucideIcon,
 } from 'lucide-vue-next'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/roadmap.vue` around lines 2 - 26, The build fails because the
Github icon was removed from the import list but is still referenced in the
template as <Github />, so restore the Github symbol to the lucide-vue-next
import statement (the same import block that currently lists ArrowRight, Check,
Hammer, Telescope, etc.) so the template's <Github class="h-3.5 w-3.5" />
resolves at build/runtime.
🧹 Nitpick comments (6)
app/pages/roadmap.vue (1)

49-49: Unused session variable after navigation refactor.

The session variable is fetched but not used since PublicNavBar handles session state.

♻️ Proposed fix to remove dead code
-const { data: session } = await authClient.useSession(useFetch)
-
 // ─── Roadmap data ──────────────────────────────────────────
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/roadmap.vue` at line 49, Remove the dead session fetch by deleting
the unused const { data: session } = await authClient.useSession(useFetch) call
(and any now-unused imports like useFetch if applicable); PublicNavBar handles
session state so no local session fetch is needed—ensure you only remove that
line and related unused bindings, leaving other authClient usages intact.
app/pages/catalog/index.vue (1)

37-37: Unused session variable after navigation refactor.

The session variable is fetched but no longer used in this component since the navigation was replaced with PublicNavBar (which handles its own session fetching). This adds an unnecessary API call.

♻️ Proposed fix to remove dead code
-const { data: session } = await authClient.useSession(useFetch)
-
 // ─── Fetch all catalog items ──────────────────────
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` at line 37, The call that assigns const { data:
session } = await authClient.useSession(useFetch) is now dead after the
navigation refactor; remove that line (and any now-unused local variable or
import related to session/useSession) so the unnecessary session fetch and API
call are eliminated; search for authClient.useSession or the session binding in
this component (index.vue) and delete the assignment and any unused
references/imports (e.g., useFetch or session) to keep the component clean.
app/pages/blog/index.vue (1)

31-31: Unused session variable after navigation refactor.

Same issue as other pages - the session variable is fetched but not used since PublicNavBar handles session state internally.

♻️ Proposed fix to remove dead code
-const { data: session } = await authClient.useSession(useFetch)
-
 const { data: posts } = await useAsyncData('blog-posts', () =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/index.vue` at line 31, Remove the dead fetch of session:
delete the unused const { data: session } = await
authClient.useSession(useFetch) call (and any now-unused imports related only to
that call) since PublicNavBar manages session state; ensure no other code
references session/useSession in this file (e.g., check for useFetch/authClient
imports) and remove them if unused.
app/pages/blog/[...slug].vue (1)

63-63: Unused session variable after navigation refactor.

The session variable is fetched but no longer used in this component since PublicNavBar handles its own session state.

♻️ Proposed fix to remove dead code
-const { data: session } = await authClient.useSession(useFetch)
 </script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/`[...slug].vue at line 63, Remove the now-unused session
retrieval: delete the line that calls authClient.useSession (const { data:
session } = await authClient.useSession(useFetch)) since PublicNavBar manages
session state; also remove any related unused imports or variables tied only to
that call to avoid dead code. Ensure no other code in this component references
session before removing.
app/pages/docs/[...slug].vue (1)

139-172: Consider using PublicNavBar for consistency.

The docs index page (docs/index.vue) was updated to use the shared PublicNavBar component, but this slug page still uses inline navigation markup. For maintainability and visual consistency, consider updating this file to also use PublicNavBar.

♻️ Suggested refactor to use PublicNavBar
-    <!-- ───── Nav ───── -->
-    <nav
-      class="fixed inset-x-0 top-0 z-50 border-b border-white/[0.06] bg-[`#09090b`]/80 backdrop-blur-xl"
-    >
-      <div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-6">
-        <NuxtLink to="/" class="text-[15px] font-semibold tracking-tight">Reqcore</NuxtLink>
-        <div class="flex items-center gap-5 text-[13px] text-white/60">
-          <NuxtLink to="/roadmap" class="transition hover:text-white">Roadmap</NuxtLink>
-          <NuxtLink to="/catalog" class="transition hover:text-white">Features</NuxtLink>
-          <NuxtLink to="/blog" class="transition hover:text-white">Blog</NuxtLink>
-          <NuxtLink to="/docs" class="text-white transition">Docs</NuxtLink>
-          <a
-            href="https://github.com/reqcore-inc/reqcore"
-            target="_blank"
-            class="transition hover:text-white"
-          >
-            <Github class="size-4" />
-          </a>
-          <NuxtLink
-            v-if="session?.user"
-            to="/dashboard"
-            class="rounded-md bg-white/10 px-3 py-1 text-white transition hover:bg-white/15"
-          >
-            Dashboard
-          </NuxtLink>
-          <NuxtLink
-            v-else
-            to="/auth/sign-in"
-            class="rounded-md bg-white/10 px-3 py-1 text-white transition hover:bg-white/15"
-          >
-            Sign In
-          </NuxtLink>
-        </div>
-      </div>
-    </nav>
+    <!-- ───── Nav ───── -->
+    <PublicNavBar active-page="docs" />

You would also need to remove the Github import from lucide-vue-next since it would no longer be used in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/docs/`[...slug].vue around lines 139 - 172, Replace the inline
navigation block with the shared PublicNavBar component to match docs/index.vue:
remove the entire <nav>…</nav> markup in this file and add the PublicNavBar
component (ensure it's imported and registered as used in the template),
preserve any props or reactive references needed (e.g., session) by passing them
to PublicNavBar if required, and delete the now-unused Github import from
lucide-vue-next; verify NuxtLink usage is handled by PublicNavBar so no
duplicate links remain.
app/components/ConsentBanner.vue (1)

14-16: Consider adding accessibility attributes for the consent banner.

For GDPR consent banners, screen reader users benefit from proper ARIA attributes. Consider adding role="dialog", aria-modal="false", and aria-labelledby to make the banner more accessible.

♿ Suggested accessibility improvements
     <div
       v-if="needsConsent"
+      role="dialog"
+      aria-modal="false"
+      aria-labelledby="consent-title"
+      aria-describedby="consent-description"
       class="fixed bottom-4 right-4 z-50 max-w-sm rounded-lg border border-white/10 bg-zinc-900/95 px-4 py-3 shadow-2xl backdrop-blur-md sm:right-6 sm:bottom-6"
     >
-      <p class="mb-1 text-[11px] font-semibold uppercase tracking-wider text-white/40">A small ask</p>
-      <p class="text-[13px] leading-relaxed text-white/70">
+      <p id="consent-title" class="mb-1 text-[11px] font-semibold uppercase tracking-wider text-white/40">A small ask</p>
+      <p id="consent-description" class="text-[13px] leading-relaxed text-white/70">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ConsentBanner.vue` around lines 14 - 16, The consent banner
rendered in ConsentBanner.vue (the <div v-if="needsConsent" ...> root) lacks
ARIA attributes; update that root element to include role="dialog",
aria-modal="false", and aria-labelledby referencing the id of the visible banner
title (create or use an id like consentBannerTitle) and optionally
aria-describedby for descriptive text; ensure the title element inside the
component has the matching id so screen readers announce the banner correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/PublicNavBar.vue`:
- Around line 55-68: The Blog and Docs NuxtLink entries use hardcoded paths
(to="/blog" and to="/docs") while other links use localePath(), causing
inconsistent locale routing; update the two NuxtLink components to use
localePath('blog') and localePath('docs') (or localePath({ name: 'blog' } if
your routes are named) for their "to" bindings, keep the existing :class logic
and activePage checks, and ensure localePath is imported/available in the
component where NuxtLink, activePage, and other navigation links are defined.

In `@app/composables/usePostHogIdentity.ts`:
- Around line 17-34: The watchers in usePostHogIdentity (and the plugin
callbacks in posthog-identity.client.ts that call
posthog.identify()/posthog.group()) send identity to PostHog immediately because
of immediate: true and no consent check; update usePostHogIdentity to read
useAnalyticsConsent() and only call $posthogIdentifyUser/$posthogSetOrganization
when consent is explicitly granted (and call $posthogReset when consent is
revoked), or remove immediate: true and trigger identification when consent
becomes true; also update the plugin callbacks that call
posthog.identify()/posthog.group() to first check useAnalyticsConsent() (or a
passed consent flag) and skip or reset identity calls when consent is not
granted.

In `@content/docs/6.legal/1.privacy-policy.md`:
- Around line 55-76: The privacy text and implementation mismatch because
usePostHogIdentity.ts currently calls posthog.identify(user.id, { name,
createdAt, orgId, orgSlug }) before consent; change the implementation to defer
any posthog.identify or identify-like calls until explicit analytics consent is
granted (use the app's consent state), stop sending direct PII (user.id, name,
createdAt) and organization identifiers until consent is given, and instead
either send a consent-gated anonymized identifier or omit identity metadata
entirely; ensure the code paths in usePostHogIdentity.ts that reference
posthog.identify and properties user.id, name, createdAt, orgId, orgSlug are
updated to check consent first and only transmit allowed anonymized data after
consent.

---

Outside diff comments:
In `@app/pages/roadmap.vue`:
- Around line 2-26: The build fails because the Github icon was removed from the
import list but is still referenced in the template as <Github />, so restore
the Github symbol to the lucide-vue-next import statement (the same import block
that currently lists ArrowRight, Check, Hammer, Telescope, etc.) so the
template's <Github class="h-3.5 w-3.5" /> resolves at build/runtime.

---

Nitpick comments:
In `@app/components/ConsentBanner.vue`:
- Around line 14-16: The consent banner rendered in ConsentBanner.vue (the <div
v-if="needsConsent" ...> root) lacks ARIA attributes; update that root element
to include role="dialog", aria-modal="false", and aria-labelledby referencing
the id of the visible banner title (create or use an id like consentBannerTitle)
and optionally aria-describedby for descriptive text; ensure the title element
inside the component has the matching id so screen readers announce the banner
correctly.

In `@app/pages/blog/`[...slug].vue:
- Line 63: Remove the now-unused session retrieval: delete the line that calls
authClient.useSession (const { data: session } = await
authClient.useSession(useFetch)) since PublicNavBar manages session state; also
remove any related unused imports or variables tied only to that call to avoid
dead code. Ensure no other code in this component references session before
removing.

In `@app/pages/blog/index.vue`:
- Line 31: Remove the dead fetch of session: delete the unused const { data:
session } = await authClient.useSession(useFetch) call (and any now-unused
imports related only to that call) since PublicNavBar manages session state;
ensure no other code references session/useSession in this file (e.g., check for
useFetch/authClient imports) and remove them if unused.

In `@app/pages/catalog/index.vue`:
- Line 37: The call that assigns const { data: session } = await
authClient.useSession(useFetch) is now dead after the navigation refactor;
remove that line (and any now-unused local variable or import related to
session/useSession) so the unnecessary session fetch and API call are
eliminated; search for authClient.useSession or the session binding in this
component (index.vue) and delete the assignment and any unused
references/imports (e.g., useFetch or session) to keep the component clean.

In `@app/pages/docs/`[...slug].vue:
- Around line 139-172: Replace the inline navigation block with the shared
PublicNavBar component to match docs/index.vue: remove the entire <nav>…</nav>
markup in this file and add the PublicNavBar component (ensure it's imported and
registered as used in the template), preserve any props or reactive references
needed (e.g., session) by passing them to PublicNavBar if required, and delete
the now-unused Github import from lucide-vue-next; verify NuxtLink usage is
handled by PublicNavBar so no duplicate links remain.

In `@app/pages/roadmap.vue`:
- Line 49: Remove the dead session fetch by deleting the unused const { data:
session } = await authClient.useSession(useFetch) call (and any now-unused
imports like useFetch if applicable); PublicNavBar handles session state so no
local session fetch is needed—ensure you only remove that line and related
unused bindings, leaving other authClient usages intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28e44969-8bf3-4475-a11b-b03912af0f30

📥 Commits

Reviewing files that changed from the base of the PR and between 0302102 and a0d17db.

📒 Files selected for processing (14)
  • app/app.vue
  • app/components/ConsentBanner.vue
  • app/components/PublicNavBar.vue
  • app/composables/useAnalyticsConsent.ts
  • app/composables/usePostHogIdentity.ts
  • app/pages/blog/[...slug].vue
  • app/pages/blog/index.vue
  • app/pages/catalog/index.vue
  • app/pages/docs/[...slug].vue
  • app/pages/docs/index.vue
  • app/pages/roadmap.vue
  • app/plugins/posthog-identity.client.ts
  • content/docs/6.legal/1.privacy-policy.md
  • nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/plugins/posthog-identity.client.ts
  • nuxt.config.ts

Comment on lines +55 to +68
<NuxtLink
to="/blog"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'blog' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Blog
</NuxtLink>
<NuxtLink
to="/docs"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'docs' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Docs
</NuxtLink>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent use of localePath() for navigation links.

The Blog and Docs links use hardcoded paths (to="/blog" and to="/docs") while other navigation links (Features, Open Positions, Roadmap) use localePath(). This inconsistency could cause issues if additional locales are configured.

♻️ Proposed fix for consistent localization
         <NuxtLink
-          to="/blog"
+          :to="localePath('/blog')"
           class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
           :class="activePage === 'blog' ? 'text-white' : 'text-surface-400 hover:text-white'"
         >
           Blog
         </NuxtLink>
         <NuxtLink
-          to="/docs"
+          :to="localePath('/docs')"
           class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
           :class="activePage === 'docs' ? 'text-white' : 'text-surface-400 hover:text-white'"
         >
           Docs
         </NuxtLink>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<NuxtLink
to="/blog"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'blog' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Blog
</NuxtLink>
<NuxtLink
to="/docs"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'docs' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Docs
</NuxtLink>
<NuxtLink
:to="localePath('/blog')"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'blog' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Blog
</NuxtLink>
<NuxtLink
:to="localePath('/docs')"
class="rounded-md px-3 py-1.5 text-[13px] font-medium transition"
:class="activePage === 'docs' ? 'text-white' : 'text-surface-400 hover:text-white'"
>
Docs
</NuxtLink>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/PublicNavBar.vue` around lines 55 - 68, The Blog and Docs
NuxtLink entries use hardcoded paths (to="/blog" and to="/docs") while other
links use localePath(), causing inconsistent locale routing; update the two
NuxtLink components to use localePath('blog') and localePath('docs') (or
localePath({ name: 'blog' } if your routes are named) for their "to" bindings,
keep the existing :class logic and activePage checks, and ensure localePath is
imported/available in the component where NuxtLink, activePage, and other
navigation links are defined.

Comment thread app/composables/usePostHogIdentity.ts
Comment thread content/docs/6.legal/1.privacy-policy.md
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 17:32 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 17:49 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 17:54 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-74 March 9, 2026 18:00 Destroyed
@JoachimLK JoachimLK requested a review from Copilot March 9, 2026 18:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates PostHog for user analytics with full GDPR consent management, replacing the previous Plausible analytics. It adds a consent banner, server-side PostHog utilities, client-side identity syncing with Better Auth, and a reusable PublicNavBar component that consolidates duplicated inline navbars across public pages.

Changes:

  • Added PostHog client (via @posthog/nuxt) and server-side (posthog-node) analytics integration with opt-in-by-default consent management
  • Created composables and a client plugin to sync Better Auth session/org identity with PostHog, gated on user consent
  • Extracted duplicated inline navigation across public pages into a shared PublicNavBar component; added a resolveDatabaseUrl() helper for Railway PR environments in both drizzle.config.ts and server/scripts/seed.ts

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
server/utils/posthog.ts Lazily-initialized singleton server-side PostHog client with graceful shutdown
server/plugins/posthog.ts Nitro plugin that calls shutdownServerPostHog() on server close
server/scripts/seed.ts Adds resolveDatabaseUrl() helper for Railway preview env DB URL resolution
drizzle.config.ts Same resolveDatabaseUrl() logic as seed.ts to support Railway preview envs
package.json Adds @posthog/nuxt and posthog-node dependencies; adds @posthog/cli override
package-lock.json Lock file updates from adding PostHog packages
nuxt.config.ts Configures PostHog module with privacy settings; removes Plausible
app/composables/useAnalyticsConsent.ts New composable managing consent state in localStorage and PostHog opt-in/out
app/composables/usePostHogIdentity.ts Syncs Better Auth session/org with PostHog identity, gated on consent
app/plugins/posthog-identity.client.ts Client plugin applying consent, URL sanitization, and providing identity helpers
app/components/ConsentBanner.vue GDPR-compliant consent banner UI with accept/decline options
app/components/PublicNavBar.vue New reusable nav component replacing duplicated inline navbars
app/app.vue Calls usePostHogIdentity() and renders ConsentBanner
app/pages/roadmap.vue Replaces inline nav with <PublicNavBar>
app/pages/docs/index.vue Replaces inline nav with <PublicNavBar>
app/pages/docs/[...slug].vue Adds Legal section icon for sidebar
app/pages/catalog/index.vue Replaces inline nav with <PublicNavBar>
app/pages/blog/index.vue Replaces inline nav with <PublicNavBar>
app/pages/blog/[...slug].vue Replaces inline nav with <PublicNavBar>
content/docs/6.legal/1.privacy-policy.md New privacy policy document with GDPR/CCPA coverage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ArrowLeft, Github, Calendar, User } from 'lucide-vue-next'
import { ArrowLeft, Calendar, User } from 'lucide-vue-next'
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, after removing the inline navbar in blog/[...slug].vue, the session variable fetched via authClient.useSession(useFetch) (which was only used by the navbar) is now unused. It should be removed since PublicNavBar fetches the session itself.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
*
* By default, PostHog captures events. Users can opt out via this composable.
* The consent state is persisted in localStorage.
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment says "By default, PostHog captures events. Users can opt out via this composable." but this is incorrect given the configuration in nuxt.config.ts which sets opt_out_capturing_by_default: true. In reality, PostHog starts in opted-out mode and users must opt in for any capturing to occur. The docstring should read "By default, PostHog does not capture events. Users can opt in via this composable."

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +15
// Must match the key used in useAnalyticsConsent.ts
const CONSENT_STORAGE_KEY = 'reqcore-analytics-consent'
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localStorage key 'reqcore-analytics-consent' is duplicated as both CONSENT_KEY in useAnalyticsConsent.ts (line 10) and CONSENT_STORAGE_KEY in posthog-identity.client.ts (line 15). The plugin's comment even says "Must match the key used in useAnalyticsConsent.ts", making this a fragile coupling. If one key is updated, the other must also be changed manually to avoid a silent mismatch that would break consent detection. Consider exporting the constant from a shared location (e.g., a shared constants file or from the composable itself) and importing it in the plugin.

Copilot uses AI. Check for mistakes.
Comment thread drizzle.config.ts
Comment on lines +8 to +34
function resolveDatabaseUrl(): string {
const raw = process.env.DATABASE_URL ?? ''

try {
const parsed = new URL(raw)
if (parsed.hostname) return raw
}
catch {
// fall through to individual-variable reconstruction
}

const host = process.env.PGHOST ?? process.env.RAILWAY_TCP_PROXY_DOMAIN ?? ''
const port = process.env.PGPORT ?? process.env.RAILWAY_TCP_PROXY_PORT ?? '5432'
const user = process.env.PGUSER ?? 'postgres'
const password = process.env.PGPASSWORD ?? ''
const database = process.env.PGDATABASE ?? 'railway'

if (host) {
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`
}

throw new Error(
`DATABASE_URL is missing a hostname and no PGHOST fallback is available.\n`
+ `Raw DATABASE_URL: "${raw}"\n`
+ `In Railway PR environments, ensure the Postgres service variables are linked to this service.`,
)
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveDatabaseUrl() function is duplicated verbatim in both drizzle.config.ts and server/scripts/seed.ts. The only difference is the error handling when no host is found (drizzle.config throws, seed.ts returns an empty string and exits with process.exit). This shared logic should be extracted into a utility module (e.g., server/utils/resolveDatabaseUrl.ts or a shared config helper) to avoid the risk of the two implementations diverging.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 5
<script setup lang="ts">
import {
Github,
ChevronRight,
ChevronDown,
X,
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the inline navbar was replaced with <PublicNavBar active-page="features" />, the session variable fetched on line 37 via authClient.useSession(useFetch) is no longer used in this file's template or script. The PublicNavBar handles session detection internally. Remove this unused fetch to avoid the extra network request.

Copilot uses AI. Check for mistakes.
Comment thread app/pages/docs/index.vue
Comment on lines 1 to 6
<script setup lang="ts">
import {
BookOpen,
Github,
ChevronRight,
Download,
Rocket,
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After replacing the inline navbar with <PublicNavBar active-page="docs" />, the session variable fetched via authClient.useSession(useFetch) at line 52 is no longer referenced in this file's template or script. Remove this unused fetch; PublicNavBar handles session detection itself.

Copilot uses AI. Check for mistakes.
Comment thread app/pages/roadmap.vue
Comment on lines 1 to 4
<script setup lang="ts">
import {
ArrowRight,
Github,
Check,
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After replacing the inline navbar with <PublicNavBar active-page="roadmap" />, the session variable fetched at line 49 via authClient.useSession(useFetch) is no longer referenced in this file's template or script. The PublicNavBar handles session detection internally. This dead fetch should be removed.

Copilot uses AI. Check for mistakes.
Comment thread package.json
"minimatch": "10.2.4",
"@isaacs/brace-expansion": "5.0.1",
"axios": ">=1.13.6",
"@posthog/cli": "0.7.1",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package.json overrides section specifies "@posthog/cli": "0.7.1", but the package-lock.json resolves @posthog/cli to version 0.5.30. The lock file was not regenerated after the override was added, so the override has no effect. Either run npm install to regenerate the lockfile, or remove the override if version 0.5.30 is acceptable.

Copilot uses AI. Check for mistakes.
Comment thread app/pages/blog/index.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ArrowRight, BookOpen, Github, Star, ChevronRight } from 'lucide-vue-next'
import { ArrowRight, BookOpen, Star, ChevronRight } from 'lucide-vue-next'
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Github icon import was removed but the session variable fetched via authClient.useSession(useFetch) on line 31 is now unused — the inline navbar that referenced session was replaced by <PublicNavBar>, which handles session fetching internally. This dead fetch can be safely removed from this file to avoid an unnecessary network request per page load.

Copilot uses AI. Check for mistakes.
@JoachimLK JoachimLK merged commit 8bd4bd5 into main Mar 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants