Advanced Features

API keys, webhooks, audit logging, impersonation, IP allowlists: things buyers ask for in security reviews. Turn them off if your MVP does not need them yet.

API keys

Workspaces can generate API keys for programmatic access. Keys are securely hashed and only shown once on creation.

How it works

  • Keys use the format sk_live_ + 32 random hex characters
  • The full key is shown once on creation, then only the prefix and last 4 characters are displayed
  • Keys are stored as SHA-256 hashes in the database
  • Each key has configurable scopes (read, write, admin) and an optional expiration date
  • Keys can be revoked (sets revoked_at)

Where to find it

  • UI: Settings page > API Keys section
  • Server actions: src/lib/api-keys/api-key-actions.ts
  • Database: supabase/006_remaining_features.sql

Permissions

  • OWNER/ADMIN can create and revoke API keys
  • All members can view API keys (masked)

API authentication middleware

A ready-to-use API key verification utility is included at src/lib/api-keys/verify-api-key.ts:

import { verifyApiKey } from "@/lib/api-keys/verify-api-key";

export async function GET(request: Request) {
  const apiKey = await verifyApiKey(request.headers.get("authorization"));
  if (!apiKey) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  if (!apiKey.scopes.includes("read")) return NextResponse.json({ error: "Insufficient scope" }, { status: 403 });

  // apiKey.workspaceId is available for scoping queries
  const { data } = await supabase
    .from("products")
    .select("*")
    .eq("workspace_id", apiKey.workspaceId);
}

An example endpoint is provided at src/app/api/v1/products/route.ts.

Where to customize

To add more API endpoints:

  1. Create a new route under src/app/api/v1/
  2. Call verifyApiKey() at the start of every handler
  3. Check apiKey.scopes against the required permission
  4. Use apiKey.workspaceId to scope all queries

Outgoing webhooks

Workspaces can register webhook URLs to receive real-time notifications when events happen.

How it works

  1. OWNER/ADMIN creates a webhook with a target URL and selected event types
  2. A signing secret is generated for verifying webhook payloads
  3. When a matching event occurs, the payload is sent to the registered URL
  4. Delivery attempts are tracked with response status, body, and retry count

Event types

Common events that can trigger webhooks:

  • product.created, product.updated, product.deleted
  • member.invited, member.role_updated
  • workspace.updated
  • And any custom events you add

Where to find it

  • UI: Settings page > Webhooks section
  • Server actions: src/lib/webhooks/webhook-actions.ts
  • Database: supabase/006_remaining_features.sql (tables: webhooks, webhook_deliveries)

Permissions

  • OWNER/ADMIN can create, update, and delete webhooks
  • All members can view webhooks

Automatic dispatch

Webhook delivery is automatically integrated with the audit log system. When you call insertAuditLog(), matching webhooks are dispatched in the background. No additional code is needed.

The dispatch logic lives in src/lib/webhooks/dispatch-webhook.ts and:

  • Queries active webhooks matching the event type
  • Signs each payload with HMAC-SHA256 using the webhook's secret
  • Sends the payload as a POST request with X-Webhook-Signature header
  • Records delivery results in webhook_deliveries (status code, response body, attempt count)
  • Fires asynchronously (non-blocking) with a 10-second timeout

Where to customize

To add new event types, update src/lib/webhooks/webhook-events.ts:

export const WEBHOOK_EVENTS = [
  "product.created",
  "product.updated",
  "product.deleted",
  "member.invited",
  "member.removed",
  "comment.created",
  "your_entity.created",  // Add your custom events
] as const;

Incoming webhooks

Workspaces can receive and log webhook events from external services.

How it works

  • External services send POST requests to your webhook endpoint
  • Events are stored with source, event type, and full JSON payload
  • Events can be marked as processed after handling

Where to find it

  • UI: Settings page > Incoming Webhooks section
  • Server actions: src/lib/incoming-webhooks/incoming-webhook-actions.ts
  • Database: supabase/006_remaining_features.sql (table: webhook_events)

Where to customize

Create API routes to receive incoming webhooks from specific services (e.g., GitHub, Slack, Stripe). Log the events to the webhook_events table for auditing and processing.

Custom fields

Workspaces can define custom fields that extend the product schema without database migrations.

Supported field types

TypeDescription
textFree-text input
numberNumeric input
dateDate picker
selectSingle-select dropdown
multi_selectMulti-select dropdown
urlURL input
booleanToggle/checkbox

How it works

  1. OWNER/ADMIN defines custom fields in Settings > Custom Fields
  2. Each field has a name, type, optional validation, and sort order
  3. Field values are stored in the custom_fields JSONB column on the products table
  4. Custom field definitions are stored in the custom_field_definitions table

Where to find it

  • UI: Settings page > Custom Fields section
  • Server actions: src/lib/custom-fields/custom-field-actions.ts
  • Database: supabase/006_remaining_features.sql

Where to customize

To add custom fields to your own entities:

  1. Add a custom_fields JSONB DEFAULT '{}' column to your table
  2. Reuse the custom_field_definitions table with a different resource_type
  3. Use the same custom-field-actions.ts functions with your resource type

Bookmarks

Users can bookmark any workspace resource for quick access.

How it works

  • Bookmarks are per-user, per-workspace
  • Each bookmark stores the resource type, ID, and display name
  • Bookmarks have a configurable sort order
  • Users can only see their own bookmarks

Where to find it

  • Server actions: src/lib/bookmarks/
  • Database: supabase/006_remaining_features.sql

Using bookmarks with your entities

import { toggleBookmark, getUserBookmarks } from "@/lib/bookmarks/bookmark-actions";

// Toggle a bookmark
await toggleBookmark(workspaceId, "task", taskId, "My Important Task");

// Get user's bookmarks
const bookmarks = await getUserBookmarks(workspaceId);

Saved views

Users can save data table configurations (filters, sorting, column visibility) as named views.

How it works

  • Views store filter, sort, and column configuration as JSON
  • Views can be shared with all workspace members or kept private
  • Only the creator can edit or delete their views

Where to find it

  • Server actions: src/lib/saved-views/
  • Database: supabase/006_remaining_features.sql

Using saved views with your data tables

Saved views work automatically if your data table uses URL-encoded state. When a user loads a saved view, the stored filters, sort, and columns are applied to the table.

Notification preferences

Users can configure how and when they receive notifications for workspace events.

Configuration options

SettingOptions
ChannelIn-app, Email
FrequencyImmediate, Daily digest, Weekly digest, None

Where to find it

  • UI: Settings page > Notification Preferences section
  • Server actions: src/lib/notifications/notification-actions.ts
  • Database: supabase/006_remaining_features.sql (table: notification_preferences)

Where to customize

To add notification delivery:

  1. Define notification types for your features
  2. Check user preferences before sending
  3. Implement delivery channels (in-app, email, push)

Scheduled actions

Create and manage scheduled tasks that execute at a specified time.

How it works

  • Actions have a type, JSON payload, and scheduled execution time
  • Actions track whether they've been executed via executed_at
  • OWNER/ADMIN can manage scheduled actions

Where to find it

  • UI: Settings page > Scheduled Actions section
  • Server actions: src/lib/scheduled-actions/scheduled-action-actions.ts
  • Database: supabase/006_remaining_features.sql

Where to customize

To process scheduled actions, set up a cron job or edge function that:

  1. Queries for actions where scheduled_for <= now() and executed_at IS NULL
  2. Processes each action based on its action_type
  3. Sets executed_at to mark completion

IP allowlist (Enterprise)

Enterprise workspaces can restrict access to specific IP address ranges.

How it works

  • OWNER adds CIDR ranges (e.g., 192.168.1.0/24) with descriptions
  • Each entry is stored in the workspace_ip_allowlist table
  • Only available on the Enterprise plan

Where to find it

  • UI: Settings page > IP Allowlist section (Enterprise only)
  • Server actions: src/lib/ip-allowlist/ip-allowlist-actions.ts
  • Database: supabase/006_remaining_features.sql

How enforcement works

IP restrictions are automatically enforced in the workspace layout (src/app/(app)/w/[workspaceSlug]/layout.tsx):

  1. On every workspace page load, the layout fetches the workspace's IP allowlist
  2. The client IP is read from x-forwarded-for headers
  3. The IP is checked against all CIDR ranges using src/lib/ip-allowlist/check-ip.ts
  4. If blocked, an "Access Denied" page is shown instead of the workspace content
  5. The Settings page is exempt so admins can't lock themselves out

Where to customize

The IP check utility supports both exact IPs and CIDR ranges with proper subnet masking. To add stricter enforcement (e.g., block API requests too), use the same isIPAllowed() function in your API routes.

Impersonation (OWNER only)

Workspace OWNERs can impersonate other workspace members for debugging and support purposes.

Where to find it

  • Server actions: src/lib/impersonation/

Security considerations

  • Only OWNERs have the impersonateUsers permission
  • All actions taken while impersonating should be clearly logged in the audit trail
  • Consider adding a visible banner when a user is being impersonated

Feature summary by plan

FeatureStarterProEnterprise
API Keysyesyesyes
Outgoing Webhooksyesyesyes
Custom Fieldsyesyesyes
Bookmarks & Saved Viewsyesyesyes
Notificationsyesyesyes
Scheduled Actionsyesyesyes
SSO----yes
IP Allowlist----yes
Impersonation----yes