Integrations

Supabase

Supabase
BackendStableServer-side

Store analytics in Supabase via PostgREST API. Works with any PostgREST-compatible endpoint for flexible Postgres storage.

Nextlytics is a server-side analytics library for Next.js. No client JavaScript, no cookies, GDPR compliant. Learn more →

Configuration

import { Nextlytics } from "@nextlytics/core/server";
import { postgrestBackend } from "@nextlytics/core/backends/postgrest";
// Optional: import Supabase client to track authenticated users
import { createClient } from "@/lib/supabase/server";
 
export const { middleware, analytics } = Nextlytics({
  backends: [
    postgrestBackend({
      // Required. Your Supabase project URL + /rest/v1
      url: process.env.SUPABASE_URL! + "/rest/v1",
 
      // Required. Your Supabase service role key (for server-side writes)
      apiKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
 
      // Optional. Table name (default: "analytics")
      tableName: "analytics",
    }),
  ],
  // Optional but recommended: identify authenticated users
  callbacks: {
    async getUser() {
      const supabase = await createClient();
      const {
        data: { user },
      } = await supabase.auth.getUser();
      if (!user) return null;
      return {
        userId: user.id,
        traits: { email: user.email },
      };
    },
  },
});

Generic PostgREST

This backend works with any PostgREST-compatible API, not just Supabase:

postgrestBackend({
  url: "https://your-postgrest-server.com",
  // apiKey is optional for non-Supabase PostgREST
});

Table Schema

Create the analytics table in Supabase SQL editor:

CREATE TABLE IF NOT EXISTS analytics (
  event_id TEXT PRIMARY KEY,
  timestamp TIMESTAMPTZ NOT NULL,
  type TEXT NOT NULL,
  host TEXT,
  path TEXT,
  method TEXT,
  user_id TEXT,
  anonymous_user_id TEXT,
  user_email TEXT,
  user_name TEXT,
  ip INET,
  referer TEXT,
  user_agent TEXT,
  locale TEXT,
  parent_event_id TEXT,
  server_context JSONB,
  client_context JSONB,
  user_traits JSONB,
  properties JSONB
);
 
-- Indexes for common queries
CREATE INDEX idx_analytics_timestamp ON analytics(timestamp DESC);
CREATE INDEX idx_analytics_type ON analytics(type);
CREATE INDEX idx_analytics_user_id ON analytics(user_id);
 
-- Enable Row Level Security (recommended)
ALTER TABLE analytics ENABLE ROW LEVEL SECURITY;
 
-- Policy for service role (server-side writes)
CREATE POLICY "Service role can insert" ON analytics
  FOR INSERT TO service_role WITH CHECK (true);
 
CREATE POLICY "Service role can update" ON analytics
  FOR UPDATE TO service_role USING (true);

If you're using Prisma, add this model to your schema.prisma instead:

model Analytics {
  event_id          String   @id
  timestamp         DateTime @db.Timestamptz()
  type              String
  host              String?
  path              String?
  method            String?
  user_id           String?
  anonymous_user_id String?
  user_email        String?
  user_name         String?
  ip                String?  @db.Inet
  referer           String?
  user_agent        String?
  locale            String?
  parent_event_id   String?
  server_context    Json?    @db.JsonB
  client_context    Json?    @db.JsonB
  user_traits       Json?    @db.JsonB
  properties        Json?    @db.JsonB
 
  @@index([timestamp(sort: Desc)])
  @@index([type])
  @@index([user_id])
  @@map("analytics")
}

Then run npx prisma db push or add a migration with npx prisma migrate dev.

Event Updates

The PostgREST backend supports event updates. When client context becomes available, Nextlytics updates the existing event via PATCH:

// Initial insert
POST /rest/v1/analytics
{ event_id: "abc", path: "/products", ... }
 
// Later update with client context
PATCH /rest/v1/analytics?event_id=eq.abc
{ client_context: { userAgent: "...", ... } }

Querying with Supabase

Use Supabase's client library to query your analytics:

import { createClient } from "@supabase/supabase-js";
 
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
 
// Page views today
const { data } = await supabase
  .from("analytics")
  .select("path, count")
  .eq("type", "pageView")
  .gte("timestamp", new Date().toISOString().split("T")[0]);
 
// User events
const { data: userEvents } = await supabase
  .from("analytics")
  .select("*")
  .eq("user_id", "user_123")
  .order("timestamp", { ascending: false });

Real-time Subscriptions

Supabase supports real-time subscriptions on your analytics table:

const channel = supabase
  .channel("analytics")
  .on("postgres_changes", { event: "INSERT", schema: "public", table: "analytics" }, (payload) => {
    console.log("New event:", payload.new);
  })
  .subscribe();

Security

Always use the service role key for server-side writes. Never expose it to the client.

For read access, create appropriate RLS policies:

-- Allow authenticated users to read their own events
CREATE POLICY "Users can read own events" ON analytics
  FOR SELECT TO authenticated
  USING (user_id = auth.uid()::text);

Limitations

  • Service role required: Writes need the service role key (not the anon key)
  • PostgREST API limits: Large batch inserts may hit API limits
  • No transactions: Each event is inserted individually

Ready to add server-side analytics?

Get started with Nextlytics in 3 simple steps.