Next.js Supabase V3 (#463)

Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
This commit is contained in:
Giancarlo Buomprisco
2026-03-24 13:40:38 +08:00
committed by GitHub
parent 4912e402a3
commit 7ebff31475
840 changed files with 71395 additions and 20095 deletions

View File

@@ -0,0 +1,363 @@
---
status: "published"
title: 'Understanding Analytics and App Events in MakerKit'
label: 'Analytics and Events'
description: 'Learn how the Analytics and App Events systems work together to provide centralized, maintainable event tracking in your MakerKit SaaS application.'
order: 0
---
MakerKit separates event emission from analytics tracking through two interconnected systems: **App Events** for broadcasting important occurrences in your app, and **Analytics** for tracking user behavior. This separation keeps your components clean and your analytics logic centralized.
## Why Centralized Analytics
Scattering `analytics.trackEvent()` calls throughout your codebase creates maintenance problems. When you need to change event names, add properties, or switch providers, you hunt through dozens of files.
The centralized approach solves this:
```typescript
// Instead of this (scattered analytics)
function CheckoutButton() {
const handleClick = () => {
analytics.trackEvent('checkout_started', { plan: 'pro' });
analytics.identify(userId);
mixpanel.track('Checkout Started');
// More provider-specific code...
};
}
// Do this (centralized via App Events)
function CheckoutButton() {
const { emit } = useAppEvents();
const handleClick = () => {
emit({ type: 'checkout.started', payload: { planId: 'pro' } });
};
}
```
The analytics mapping lives in one place: `apps/web/components/analytics-provider.tsx`.
## How It Works
The system has three parts:
1. **App Events Provider**: A React Context that provides `emit`, `on`, and `off` functions
2. **Analytics Provider**: Subscribes to App Events and maps them to analytics calls
3. **Analytics Manager**: Dispatches events to all registered analytics providers
```
Component Analytics Provider Providers
│ │ │
│ emit('checkout.started') │ │
│────────────────────────────▶│ │
│ │ analytics.trackEvent │
│ │────────────────────────▶│
│ │ analytics.identify │
│ │────────────────────────▶│
```
## Emitting Events
Use the `useAppEvents` hook to emit events from any component:
```typescript
import { useAppEvents } from '@kit/shared/events';
function FeatureButton() {
const { emit } = useAppEvents();
const handleClick = () => {
emit({
type: 'feature.used',
payload: { featureName: 'export' }
});
};
return <button onClick={handleClick}>Export</button>;
}
```
The event is broadcast to all listeners, including the analytics provider.
## Default Event Types
MakerKit defines these base event types in `@kit/shared/events`:
```typescript
interface BaseAppEventTypes {
'user.signedIn': { userId: string };
'user.signedUp': { method: 'magiclink' | 'password' };
'user.updated': Record<string, never>;
'checkout.started': { planId: string; account?: string };
}
```
These events are emitted automatically by MakerKit components and mapped to analytics calls.
### Event Descriptions
| Event | When Emitted | Analytics Action |
|-------|--------------|------------------|
| `user.signedIn` | After successful login | `identify(userId)` |
| `user.signedUp` | After registration | `trackEvent('user.signedUp')` |
| `user.updated` | After profile update | `trackEvent('user.updated')` |
| `checkout.started` | When billing checkout begins | `trackEvent('checkout.started')` |
**Note**: The `user.signedUp` event does not fire automatically for social/OAuth signups. You may need to emit it manually in your OAuth callback handler.
## Creating Custom Events
Define custom events by extending `ConsumerProvidedEventTypes`:
```typescript {% title="lib/events/custom-events.ts" %}
import { ConsumerProvidedEventTypes } from '@kit/shared/events';
export interface MyAppEvents extends ConsumerProvidedEventTypes {
'feature.used': { featureName: string; duration?: number };
'project.created': { projectId: string; template: string };
'export.completed': { format: 'csv' | 'json' | 'pdf'; rowCount: number };
}
```
Use the typed hook in your components:
```typescript
import { useAppEvents } from '@kit/shared/events';
import type { MyAppEvents } from '~/lib/events/custom-events';
function ProjectForm() {
const { emit } = useAppEvents<MyAppEvents>();
const handleCreate = (project: Project) => {
emit({
type: 'project.created',
payload: {
projectId: project.id,
template: project.template,
},
});
};
}
```
TypeScript enforces the correct payload shape for each event type.
## Mapping Events to Analytics
The `AnalyticsProvider` component maps events to analytics calls. Add your custom events here:
```typescript {% title="apps/web/components/analytics-provider.tsx" %}
const analyticsMapping: AnalyticsMapping = {
'user.signedIn': (event) => {
const { userId, ...traits } = event.payload;
if (userId) {
return analytics.identify(userId, traits);
}
},
'user.signedUp': (event) => {
return analytics.trackEvent(event.type, event.payload);
},
'checkout.started': (event) => {
return analytics.trackEvent(event.type, event.payload);
},
// Add custom event mappings
'feature.used': (event) => {
return analytics.trackEvent('Feature Used', {
feature_name: event.payload.featureName,
duration: String(event.payload.duration ?? 0),
});
},
'project.created': (event) => {
return analytics.trackEvent('Project Created', {
project_id: event.payload.projectId,
template: event.payload.template,
});
},
};
```
This is the only place you need to modify when changing analytics behavior.
## Listening to Events
Beyond analytics, you can subscribe to events for other purposes:
```typescript
import { useAppEvents } from '@kit/shared/events';
import { useEffect } from 'react';
function NotificationListener() {
const { on, off } = useAppEvents();
useEffect(() => {
const handler = (event) => {
showToast(`Project ${event.payload.projectId} created!`);
};
on('project.created', handler);
return () => off('project.created', handler);
}, [on, off]);
return null;
}
```
This pattern is useful for triggering side effects like notifications, confetti animations, or feature tours.
## Direct Analytics API
While centralized events are recommended, you can use the analytics API directly when needed:
```typescript
import { analytics } from '@kit/analytics';
// Identify a user
void analytics.identify('user_123', {
email: 'user@example.com',
plan: 'pro',
});
// Track an event
void analytics.trackEvent('Button Clicked', {
button: 'submit',
page: 'settings',
});
// Track a page view (usually automatic)
void analytics.trackPageView('/dashboard');
```
Use direct calls for one-off tracking that does not warrant an event type.
## Automatic Page View Tracking
The `AnalyticsProvider` automatically tracks page views when the Next.js route changes:
```typescript
// This happens automatically in AnalyticsProvider
function useReportPageView(reportFn: (url: string) => unknown) {
const pathname = usePathname();
useEffect(() => {
const url = pathname;
reportFn(url);
}, [pathname]);
}
```
You do not need to manually track page views unless you have a specific use case.
## Common Patterns
### Track Form Submissions
```typescript
function ContactForm() {
const { emit } = useAppEvents<MyAppEvents>();
const handleSubmit = async (data: FormData) => {
await submitForm(data);
emit({
type: 'form.submitted',
payload: { formName: 'contact', fields: Object.keys(data).length },
});
};
}
```
### Track Feature Engagement
```typescript
function AIAssistant() {
const { emit } = useAppEvents<MyAppEvents>();
const startTime = useRef<number>();
const handleOpen = () => {
startTime.current = Date.now();
};
const handleClose = () => {
const duration = Date.now() - (startTime.current ?? Date.now());
emit({
type: 'feature.used',
payload: { featureName: 'ai-assistant', duration },
});
};
}
```
### Track Errors
```typescript
function ErrorBoundary({ children }) {
const { emit } = useAppEvents<MyAppEvents>();
const handleError = (error: Error) => {
emit({
type: 'error.occurred',
payload: {
message: error.message,
stack: error.stack?.slice(0, 500),
},
});
};
}
```
## Debugging Events
During development, add logging to your event handlers to verify events are emitting correctly:
```typescript {% title="apps/web/components/analytics-provider.tsx" %}
const analyticsMapping: AnalyticsMapping = {
'user.signedIn': (event) => {
if (process.env.NODE_ENV === 'development') {
console.log('[Analytics Event]', event.type, event.payload);
}
const { userId, ...traits } = event.payload;
if (userId) {
return analytics.identify(userId, traits);
}
},
'checkout.started': (event) => {
if (process.env.NODE_ENV === 'development') {
console.log('[Analytics Event]', event.type, event.payload);
}
return analytics.trackEvent(event.type, event.payload);
},
// Add logging to other handlers as needed
};
```
You can also use your analytics provider's debug mode (PostHog and GA4 both offer live event views in their dashboards).
## Best Practices
1. **Use App Events for domain events**: Business-relevant events (signup, purchase, feature use) should go through App Events
2. **Keep payloads minimal**: Only include data you will actually analyze
3. **Use consistent naming**: Follow a pattern like `noun.verb` (user.signedUp, project.created)
4. **Type your events**: Define interfaces for compile-time safety
5. **Test event emission**: Verify critical events emit during integration tests
6. **Document your events**: Maintain a list of events and their purposes
{% faq
title="Frequently Asked Questions"
items=[
{"question": "Can I use analytics without App Events?", "answer": "Yes. Import analytics from @kit/analytics and call trackEvent directly. However, the centralized approach through App Events is easier to maintain as your application grows."},
{"question": "How do I track events on the server side?", "answer": "Import analytics from @kit/analytics/server. Note that only PostHog supports server-side analytics out of the box. The App Events system is client-side only."},
{"question": "Are page views tracked automatically?", "answer": "Yes. The AnalyticsProvider component tracks page views whenever the Next.js route changes. You only need manual tracking for virtual page views in SPAs."},
{"question": "How do I debug which events are firing?", "answer": "Add a wildcard handler in your analytics mapping that logs events in development mode. You can also use browser DevTools or your analytics provider's debug mode."},
{"question": "Can I emit events from Server Components?", "answer": "No. App Events use React Context which requires a client component. Emit events from client components or use the server-side analytics API directly."},
{"question": "What happens if no analytics provider is configured?", "answer": "Events dispatch to the NullAnalyticsService which silently ignores them. Your application continues to work without errors."}
]
/%}
## Next Steps
- [Set up Google Analytics](google-analytics-provider) for marketing analytics
- [Set up PostHog](posthog-analytics-provider) for product analytics with feature flags
- [Create a custom provider](custom-analytics-provider) to integrate other services

View File

@@ -0,0 +1,360 @@
---
status: "published"
title: 'Creating a Custom Analytics Provider in MakerKit'
label: 'Custom Analytics Provider'
description: 'Build a custom analytics provider to integrate Mixpanel, Amplitude, Segment, or any analytics service with MakerKit unified analytics API.'
order: 5
---
MakerKit's analytics system is provider-agnostic. If your preferred analytics service is not included (Google Analytics, PostHog, Umami), you can create a custom provider that integrates with the unified analytics API. Events dispatched through `analytics.trackEvent()` or App Events will automatically route to your custom provider alongside any other registered providers.
## The AnalyticsService Interface
Every analytics provider must implement the `AnalyticsService` interface:
```typescript
interface AnalyticsService {
initialize(): Promise<unknown>;
identify(userId: string, traits?: Record<string, string>): Promise<unknown>;
trackPageView(path: string): Promise<unknown>;
trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
): Promise<unknown>;
}
```
| Method | Purpose |
|--------|---------|
| `initialize()` | Load scripts, set up the SDK |
| `identify()` | Associate a user ID with subsequent events |
| `trackPageView()` | Record a page view |
| `trackEvent()` | Record a custom event with properties |
All methods return Promises. Use `void` when calling from non-async contexts.
## Example: Mixpanel Provider
Here is a complete implementation for Mixpanel:
```typescript {% title="packages/analytics/src/mixpanel-service.ts" %}
import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsService } from './types';
class MixpanelService implements AnalyticsService {
private mixpanel: typeof import('mixpanel-browser') | null = null;
private token: string;
constructor(token: string) {
this.token = token;
}
async initialize(): Promise<void> {
if (typeof window === 'undefined') {
return;
}
const mixpanel = await import('mixpanel-browser');
mixpanel.init(this.token, {
track_pageview: false, // We handle this manually
persistence: 'localStorage',
});
this.mixpanel = mixpanel;
}
async identify(userId: string, traits?: Record<string, string>): Promise<void> {
if (!this.mixpanel) return;
this.mixpanel.identify(userId);
if (traits) {
this.mixpanel.people.set(traits);
}
}
async trackPageView(path: string): Promise<void> {
if (!this.mixpanel) return;
this.mixpanel.track('Page Viewed', { path });
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
): Promise<void> {
if (!this.mixpanel) return;
this.mixpanel.track(eventName, eventProperties);
}
}
export function createMixpanelService(): AnalyticsService {
const token = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
if (!token) {
console.warn('Mixpanel token not configured');
return new NullAnalyticsService();
}
return new MixpanelService(token);
}
```
Install the Mixpanel SDK:
```bash
pnpm add mixpanel-browser --filter "@kit/analytics"
```
## Registering Your Provider
Add your custom provider to the analytics manager:
```typescript {% title="packages/analytics/src/index.ts" %}
import { createAnalyticsManager } from './analytics-manager';
import { createMixpanelService } from './mixpanel-service';
import type { AnalyticsManager } from './types';
export const analytics: AnalyticsManager = createAnalyticsManager({
providers: {
mixpanel: createMixpanelService,
},
});
```
Add environment variables:
```bash {% title=".env.local" %}
NEXT_PUBLIC_MIXPANEL_TOKEN=your_mixpanel_token
```
## Using Multiple Providers
Register multiple providers to dispatch events to all of them:
```typescript {% title="packages/analytics/src/index.ts" %}
import { createAnalyticsManager } from './analytics-manager';
import { createMixpanelService } from './mixpanel-service';
import { createPostHogAnalyticsService } from '@kit/posthog/client';
export const analytics = createAnalyticsManager({
providers: {
mixpanel: createMixpanelService,
posthog: createPostHogAnalyticsService,
},
});
```
When you call `analytics.trackEvent()`, both Mixpanel and PostHog receive the event.
## Example: Amplitude Provider
Here is a skeleton for Amplitude:
```typescript {% title="packages/analytics/src/amplitude-service.ts" %}
import type { AnalyticsService } from './types';
class AmplitudeService implements AnalyticsService {
private amplitude: typeof import('@amplitude/analytics-browser') | null = null;
async initialize(): Promise<void> {
if (typeof window === 'undefined') return;
const amplitude = await import('@amplitude/analytics-browser');
const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
if (apiKey) {
amplitude.init(apiKey);
this.amplitude = amplitude;
}
}
async identify(userId: string, traits?: Record<string, string>): Promise<void> {
if (!this.amplitude) return;
this.amplitude.setUserId(userId);
if (traits) {
const identifyEvent = new this.amplitude.Identify();
Object.entries(traits).forEach(([key, value]) => {
identifyEvent.set(key, value);
});
this.amplitude.identify(identifyEvent);
}
}
async trackPageView(path: string): Promise<void> {
if (!this.amplitude) return;
this.amplitude.track('Page Viewed', { path });
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
): Promise<void> {
if (!this.amplitude) return;
this.amplitude.track(eventName, eventProperties);
}
}
export function createAmplitudeService(): AnalyticsService {
return new AmplitudeService();
}
```
## Example: Segment Provider
Segment acts as a data router to multiple destinations:
```typescript {% title="packages/analytics/src/segment-service.ts" %}
import type { AnalyticsService } from './types';
declare global {
interface Window {
analytics: {
identify: (userId: string, traits?: object) => void;
page: (name?: string, properties?: object) => void;
track: (event: string, properties?: object) => void;
};
}
}
class SegmentService implements AnalyticsService {
async initialize(): Promise<void> {
// Segment snippet is typically added via <Script> in layout
// This method can verify it's loaded
if (typeof window === 'undefined' || !window.analytics) {
console.warn('Segment analytics not loaded');
}
}
async identify(userId: string, traits?: Record<string, string>): Promise<void> {
window.analytics?.identify(userId, traits);
}
async trackPageView(path: string): Promise<void> {
window.analytics?.page(undefined, { path });
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
): Promise<void> {
window.analytics?.track(eventName, eventProperties);
}
}
export function createSegmentService(): AnalyticsService {
return new SegmentService();
}
```
## Server-Side Providers
For server-side analytics, create a separate service file:
```typescript {% title="packages/analytics/src/mixpanel-server.ts" %}
import Mixpanel from 'mixpanel';
import type { AnalyticsService } from './types';
class MixpanelServerService implements AnalyticsService {
private mixpanel: Mixpanel.Mixpanel | null = null;
async initialize(): Promise<void> {
const token = process.env.MIXPANEL_TOKEN; // Note: no NEXT_PUBLIC_ prefix
if (token) {
this.mixpanel = Mixpanel.init(token);
}
}
async identify(userId: string, traits?: Record<string, string>): Promise<void> {
if (!this.mixpanel || !traits) return;
this.mixpanel.people.set(userId, traits);
}
async trackPageView(path: string): Promise<void> {
// Server-side page views are uncommon
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
): Promise<void> {
if (!this.mixpanel) return;
this.mixpanel.track(eventName, eventProperties);
}
}
```
Register in `packages/analytics/src/server.ts`:
```typescript {% title="packages/analytics/src/server.ts" %}
import 'server-only';
import { createAnalyticsManager } from './analytics-manager';
import { createMixpanelServerService } from './mixpanel-server';
export const analytics = createAnalyticsManager({
providers: {
mixpanel: createMixpanelServerService,
},
});
```
## The NullAnalyticsService
When no providers are configured, MakerKit uses a null service that silently ignores all calls:
```typescript
const NullAnalyticsService: AnalyticsService = {
initialize: () => Promise.resolve(),
identify: () => Promise.resolve(),
trackPageView: () => Promise.resolve(),
trackEvent: () => Promise.resolve(),
};
```
Your provider factory can return this when misconfigured to avoid errors.
## Best Practices
1. **Dynamic imports**: Load SDKs dynamically to reduce bundle size
2. **Environment checks**: Always check `typeof window` before accessing browser APIs
3. **Graceful degradation**: Return early if the SDK fails to load
4. **Typed properties**: Define TypeScript interfaces for your event properties
5. **Consistent naming**: Use the same event names across all providers
## Troubleshooting
### Provider not receiving events
- Verify the provider is registered in `createAnalyticsManager`
- Check that `initialize()` completes without errors
- Confirm environment variables are set
### TypeScript errors
- Ensure your class implements all methods in `AnalyticsService`
- Check that return types are `Promise<unknown>` or more specific
### Events delayed or missing
- Some providers batch events. Check provider-specific settings
- Verify the provider SDK is loaded before events are sent
{% faq
title="Frequently Asked Questions"
items=[
{"question": "Can I use the same provider for client and server?", "answer": "It depends on the SDK. Some analytics SDKs (like PostHog) offer both client and server versions. Others (like Mixpanel) have separate packages. Create separate service files for each environment."},
{"question": "How do I test my custom provider?", "answer": "Add console.log statements in each method during development. Most analytics dashboards also have a debug or live events view."},
{"question": "Can I conditionally load providers?", "answer": "Yes. Your factory function can check environment variables or feature flags and return NullAnalyticsService when the provider should be disabled."},
{"question": "How do I handle errors in providers?", "answer": "Wrap SDK calls in try-catch blocks. Log errors but do not throw them, as this would affect other providers in the chain."}
]
/%}
## Next Steps
- [Learn about Analytics and Events](analytics-and-events) for event patterns
- [See Google Analytics](google-analytics-provider) as a reference implementation
- [Try PostHog](posthog-analytics-provider) for a full-featured option

View File

@@ -0,0 +1,148 @@
---
status: "published"
title: 'Using the Google Analytics Provider in Next.js Supabase Turbo'
label: 'Google Analytics'
description: 'Add Google Analytics 4 (GA4) to your MakerKit application for page views, user tracking, and conversion measurement.'
order: 2
---
Google Analytics 4 (GA4) provides web analytics focused on marketing attribution, conversion tracking, and audience insights. Use it when your marketing team needs Google's ecosystem for ad optimization and reporting.
## Prerequisites
Before starting, you need:
- A Google Analytics 4 property ([create one here](https://analytics.google.com/))
- Your GA4 Measurement ID (format: `G-XXXXXXXXXX`)
Find your Measurement ID in GA4: **Admin > Data Streams > Select your stream > Measurement ID**.
## Installation
Install the Google Analytics plugin using the MakerKit CLI:
```bash
npx @makerkit/cli@latest plugins add google-analytics
```
Our codemod will wire up the plugin in your project, so you don't have to do anything manually. Please review the changes with `git diff`.
Please add your Measurement ID to environment variables:
```bash {% title="apps/web/.env.local" %}
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
```
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | Yes | Your GA4 Measurement ID |
| `NEXT_PUBLIC_GA_DISABLE_PAGE_VIEWS_TRACKING` | No | Set to `true` to disable automatic page view tracking |
| `NEXT_PUBLIC_GA_DISABLE_LOCALHOST_TRACKING` | No | Set to `true` to disable tracking on localhost |
### Development Configuration
Disable localhost tracking to avoid polluting your analytics during development:
```bash {% title=".env.local" %}
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GA_DISABLE_LOCALHOST_TRACKING=true
```
## Verification
After configuration, verify the integration:
1. Open your application in the browser
2. Open Chrome DevTools > Network tab
3. Filter by `google-analytics` or `gtag`
4. Navigate between pages and confirm requests are sent
5. Check GA4 Realtime reports to see your session
## Using with Other Providers
Google Analytics can run alongside other providers. Events dispatch to all registered providers:
```typescript {% title="packages/analytics/src/index.ts" %}
import { createGoogleAnalyticsService } from '@kit/google-analytics';
import { createPostHogAnalyticsService } from '@kit/posthog/client';
import { createAnalyticsManager } from './analytics-manager';
export const analytics = createAnalyticsManager({
providers: {
'google-analytics': createGoogleAnalyticsService,
posthog: createPostHogAnalyticsService,
},
});
```
This setup is common when marketing uses GA4 for attribution while product uses PostHog for behavior analysis.
## Tracked Events
With the default configuration, Google Analytics receives:
- **Page views**: Automatically tracked on route changes
- **User identification**: When `analytics.identify()` is called
- **Custom events**: All events passed to `analytics.trackEvent()`
Events from the App Events system (user.signedUp, checkout.started, etc.) are forwarded to GA4 through the analytics mapping.
## GDPR Considerations
Google Analytics sets cookies and requires user consent in the EU. Integrate with the [Cookie Banner component](/docs/next-supabase-turbo/components/cookie-banner) to manage consent:
```typescript
import { useCookieConsent, ConsentStatus } from '@kit/ui/cookie-banner';
import { analytics } from '@kit/analytics';
import { useEffect } from 'react';
function AnalyticsGate({ children }) {
const { status } = useCookieConsent();
useEffect(() => {
if (status === ConsentStatus.Accepted) {
// GA is initialized automatically when consent is given
// You may want to delay initialization until consent
}
}, [status]);
return children;
}
```
Consider using [Umami](umami-analytics-provider) for cookie-free, GDPR-compliant analytics.
## Troubleshooting
### Events not appearing in GA4
- Verify your Measurement ID is correct
- Check that `NEXT_PUBLIC_GA_DISABLE_LOCALHOST_TRACKING` is not `true` in production
- GA4 has a delay of up to 24-48 hours for some reports. Use Realtime for immediate verification
### Duplicate page views
- Ensure you have not called `trackPageView` manually. MakerKit tracks page views automatically
### Ad blockers
- Ad blockers often block Google Analytics. Consider using a proxy or server-side tracking for critical metrics
{% faq
title="Frequently Asked Questions"
items=[
{"question": "Does MakerKit support Universal Analytics?", "answer": "No. Universal Analytics was sunset by Google in July 2023. MakerKit only supports Google Analytics 4 (GA4)."},
{"question": "Can I use Google Tag Manager instead?", "answer": "Yes, but you would need to create a custom analytics provider. The built-in plugin uses gtag.js directly."},
{"question": "How do I track conversions?", "answer": "Use analytics.trackEvent() with your conversion event name. Configure the event as a conversion in GA4 Admin > Events > Mark as conversion."},
{"question": "Is server-side tracking supported?", "answer": "No. The Google Analytics plugin is client-side only. Use the Measurement Protocol API directly if you need server-side GA4 tracking."}
]
/%}
## Next Steps
- [Learn about Analytics and Events](analytics-and-events) for custom event tracking
- [Add PostHog](posthog-analytics-provider) for product analytics
- [Create a custom provider](custom-analytics-provider) for other services

View File

@@ -0,0 +1,32 @@
---
status: "published"
title: 'Using the Meshes Analytics Provider in Next.js Supabase Turbo'
label: 'Meshes'
description: 'Add Meshes to your MakerKit application for event tracking and analytics.'
order: 6
---
[Meshes](https://meshes.io/) is a platform for event tracking for user engagement and conversions. It captures custom events (signups, plan upgrades, feature usage) so you can measure what matters without building your own event pipeline.
## Installation
Install the Meshes plugin using the MakerKit CLI:
```bash
npx @makerkit/cli@latest plugins add meshes-analytics
```
The Makerkit CLI will automatically wire up the plugin in your project, so you don't have to do anything manually
The codemod is very complex, so please review the changes with `git diff` and commit them.
## Environment Variables
Set the analytics provider and Meshes configuration:
```bash title=".env.local"
# Meshes configuration
NEXT_PUBLIC_MESHES_PUBLISHABLE_KEY=your_api_key_here
```
Please [read the Meshes documentation](https://meshes.io/docs) for more information on how to use the Meshes analytics provider.

View File

@@ -0,0 +1,202 @@
---
status: "published"
title: 'Using the PostHog Analytics Provider in Next.js Supabase Turbo'
label: 'PostHog'
description: 'Add PostHog to your MakerKit application for product analytics, session replay, and feature flags with client-side and server-side support.'
order: 3
---
PostHog provides product analytics, session replay, feature flags, and A/B testing in one platform.
Unlike marketing-focused tools, PostHog helps you understand how users interact with your product.
Posthog supports both client-side and server-side tracking, and can be self-hosted for full data control.
## Prerequisites
Before starting:
- Create a PostHog account at [posthog.com](https://posthog.com)
- Note your Project API Key (starts with `phc_`)
- Choose your region: `eu.posthog.com` or `us.posthog.com`
Find your API key in PostHog: **Project Settings > Project API Key**.
## Installation
Install the PostHog plugin using the MakerKit CLI:
```bash
npx @makerkit/cli@latest plugins add posthog
```
Our codemod will wire up the plugin in your project, so you don't have to do anything manually. Please review the changes with `git diff`.
Add environment variables:
```bash {% title=".env.local" %}
NEXT_PUBLIC_POSTHOG_KEY=phc_your_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com
```
Use `https://us.posthog.com` if your project is in the US region.
## Server-Side Configuration
PostHog supports server-side analytics for tracking events in API routes and Server Actions:
Use server-side tracking in your code:
```typescript
import { analytics } from '@kit/analytics/server';
export async function createProject(data: ProjectData) {
const project = await db.projects.create(data);
await analytics.trackEvent('project.created', {
projectId: project.id,
userId: data.userId,
});
return project;
}
```
## Bypassing Ad Blockers with Ingestion Rewrites
Ad blockers frequently block PostHog. Use Next.js rewrites to proxy requests through your domain:
### Step 1: Add the Ingestion URL
```bash {% title=".env.local" %}
NEXT_PUBLIC_POSTHOG_KEY=phc_your_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com
NEXT_PUBLIC_POSTHOG_INGESTION_URL=http://localhost:3000/ingest
```
In production, replace `localhost:3000` with your domain.
### Step 2: Configure Next.js Rewrites
Add rewrites to your Next.js configuration:
```javascript {% title="apps/web/next.config.mjs" %}
/** @type {import('next').NextConfig} */
const config = {
// Required for PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
async rewrites() {
// Change 'eu' to 'us' if using the US region
return [
{
source: '/ingest/static/:path*',
destination: 'https://eu-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://eu.i.posthog.com/:path*',
},
];
},
};
export default config;
```
### Step 3: Exclude Ingestion Endpoint from Middleware
Ensure the ingestion endpoint is excluded from the middleware matcher:
```typescript {% title="apps/web/proxy.ts" %}
export const config = {
matcher: [
'/((?!_next/static|_next/image|images|locales|assets|ingest/*|api/*).*)',
],
};
```
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_POSTHOG_KEY` | Yes | Your PostHog Project API Key |
| `NEXT_PUBLIC_POSTHOG_HOST` | Yes | PostHog host (`https://eu.posthog.com` or `https://us.posthog.com`) |
| `NEXT_PUBLIC_POSTHOG_INGESTION_URL` | No | Proxy URL to bypass ad blockers (e.g., `https://yourdomain.com/ingest`) |
## Verification
After configuration:
1. Open your application
2. Navigate between pages
3. Open PostHog > Activity > Live Events
4. Confirm page views and events appear
If using ingestion rewrites, check the Network tab for requests to `/ingest` instead of `posthog.com`.
## Using with Other Providers
PostHog works alongside other analytics providers:
```typescript {% title="packages/analytics/src/index.ts" %}
import { createPostHogAnalyticsService } from '@kit/posthog/client';
import { createGoogleAnalyticsService } from '@kit/google-analytics';
import { createAnalyticsManager } from './analytics-manager';
export const analytics = createAnalyticsManager({
providers: {
posthog: createPostHogAnalyticsService,
'google-analytics': createGoogleAnalyticsService,
},
});
```
## PostHog Features Beyond Analytics
PostHog offers additional features beyond event tracking:
- **Session Replay**: Watch user sessions to debug issues
- **Feature Flags**: Control feature rollouts
- **A/B Testing**: Run experiments on UI variants
- **Surveys**: Collect user feedback
These features are available in the PostHog dashboard once you are capturing events.
For monitoring features (error tracking), see the [PostHog Monitoring guide](/docs/next-supabase-turbo/monitoring/posthog).
## Troubleshooting
### Events not appearing
- Verify your API key starts with `phc_`
- Confirm the host matches your project region (EU vs US)
- Check for ad blockers if not using ingestion rewrites
### CORS errors with ingestion rewrites
- Ensure `skipTrailingSlashRedirect: true` is set in next.config.mjs
- Verify the rewrite destination matches your region
### Server-side events not appearing
- Ensure you import from `@kit/analytics/server`, not `@kit/analytics`
- Server-side tracking requires the same environment variables
{% faq
title="Frequently Asked Questions"
items=[
{"question": "Should I use the EU or US region?", "answer": "Use the EU region (eu.posthog.com) if you have European users and want GDPR-compliant data residency. The US region may have slightly lower latency for US-based users."},
{"question": "Can I self-host PostHog?", "answer": "Yes. PostHog can be self-hosted using Docker. Update NEXT_PUBLIC_POSTHOG_HOST to your self-hosted instance URL."},
{"question": "How do I enable session replay?", "answer": "Session replay is enabled by default in PostHog. Configure recording settings in PostHog > Project Settings > Session Replay."},
{"question": "Do ingestion rewrites work on Vercel?", "answer": "Yes. The rewrites in next.config.mjs work on Vercel and other Next.js hosting platforms."},
{"question": "Is PostHog GDPR compliant?", "answer": "PostHog can be GDPR compliant. Use the EU region for data residency, enable cookie-less tracking, and integrate with a consent management solution."}
]
/%}
## Next Steps
- [Learn about Analytics and Events](analytics-and-events) for custom event tracking
- [Set up PostHog for monitoring](/docs/next-supabase-turbo/monitoring/posthog)
- [Try Umami](umami-analytics-provider) for simpler, privacy-focused analytics

View File

@@ -0,0 +1,151 @@
---
status: "published"
title: 'Using the Umami Analytics Provider in Next.js Supabase Turbo'
label: 'Umami'
description: 'Add Umami to your MakerKit application for privacy-focused, cookie-free analytics that comply with GDPR without consent banners.'
order: 4
---
Umami is a privacy-focused analytics platform that tracks page views and events without cookies.
Because it does not use cookies or collect personal data, you can use Umami without displaying cookie consent banners in the EU. Umami can be self-hosted for complete data ownership or used via Umami Cloud.
## Why Choose Umami
| Feature | Umami | Google Analytics |
|---------|-------|------------------|
| Cookies | None | Yes |
| GDPR consent required | No | Yes |
| Self-hosting | Yes | No |
| Pricing | Free (self-hosted) or paid cloud | Free |
| Data ownership | Full | Google |
| Session replay | No | No |
| Feature flags | No | No |
**Use Umami when**: You want simple, clean metrics without privacy concerns. Ideal for landing pages, documentation sites, and applications where marketing attribution is not critical.
## Prerequisites
Before starting:
- Create an Umami account at [umami.is](https://umami.is) or self-host
- Create a website in your Umami dashboard
- Note your **Website ID** and **Script URL**
In Umami Cloud, find these at: **Settings > Websites > Your Website > Edit**.
## Installation
Install the Umami plugin using the MakerKit CLI:
```bash
npx @makerkit/cli@latest plugins add umami
```
Our codemod will wire up the plugin in your project, so you don't have to do anything manually. Please review the changes with `git diff`.
Add environment variables:
```bash {% title=".env.local" %}
NEXT_PUBLIC_UMAMI_HOST=https://cloud.umami.is/script.js
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id
```
### Self-Hosted Configuration
If self-hosting Umami, point to your instance:
```bash {% title=".env.local" %}
NEXT_PUBLIC_UMAMI_HOST=https://analytics.yourdomain.com/script.js
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id
```
Replace the URL with the path to your Umami instance's tracking script.
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_UMAMI_HOST` | Yes | URL to the Umami tracking script |
| `NEXT_PUBLIC_UMAMI_WEBSITE_ID` | Yes | Your website ID from Umami |
| `NEXT_PUBLIC_UMAMI_DISABLE_LOCALHOST_TRACKING` | No | Set to `false` to enable localhost tracking |
### Development Configuration
By default, Umami does not track localhost. Enable it for development testing:
```bash {% title=".env.local" %}
NEXT_PUBLIC_UMAMI_HOST=https://cloud.umami.is/script.js
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id
NEXT_PUBLIC_UMAMI_DISABLE_LOCALHOST_TRACKING=false
```
## Verification
After configuration:
1. Deploy to a non-localhost environment (or enable localhost tracking)
2. Open your application and navigate between pages
3. Check your Umami dashboard > Realtime
4. Confirm page views appear
## Custom Event Tracking
Umami tracks page views automatically. For custom events:
```typescript
import { analytics } from '@kit/analytics';
void analytics.trackEvent('Button Clicked', {
button: 'signup',
location: 'header',
});
```
Events appear in Umami under **Events** in your website dashboard.
## Using with Other Providers
Umami can run alongside other providers:
```typescript {% title="packages/analytics/src/index.ts" %}
import { createUmamiAnalyticsService } from '@kit/umami';
import { createPostHogAnalyticsService } from '@kit/posthog/client';
import { createAnalyticsManager } from './analytics-manager';
export const analytics = createAnalyticsManager({
providers: {
umami: createUmamiAnalyticsService,
posthog: createPostHogAnalyticsService,
},
});
```
This setup provides Umami's clean metrics alongside PostHog's product analytics.
## Troubleshooting
### No data appearing
- Verify you are not on localhost (or enable localhost tracking)
- Check that the Website ID matches your Umami dashboard
- Confirm the script URL is correct and accessible
### Ad blocker interference
Umami is sometimes blocked by ad blockers. If this is an issue:
1. Self-host Umami on a subdomain (e.g., `analytics.yourdomain.com`)
2. Use a generic script path (e.g., `/stats.js` instead of `/script.js`)
### Events not tracked
- Ensure event names and properties are strings
- Check that the event appears in Umami's Events tab, not just Page Views
## Next Steps
- [Learn about Analytics and Events](analytics-and-events) for event tracking patterns
- [Consider PostHog](posthog-analytics-provider) if you need user identification or feature flags
- [Create a custom provider](custom-analytics-provider) for other analytics services