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:
- Create a new route under
src/app/api/v1/ - Call
verifyApiKey()at the start of every handler - Check
apiKey.scopesagainst the required permission - Use
apiKey.workspaceIdto scope all queries
Outgoing webhooks
Workspaces can register webhook URLs to receive real-time notifications when events happen.
How it works
- OWNER/ADMIN creates a webhook with a target URL and selected event types
- A signing secret is generated for verifying webhook payloads
- When a matching event occurs, the payload is sent to the registered URL
- Delivery attempts are tracked with response status, body, and retry count
Event types
Common events that can trigger webhooks:
product.created,product.updated,product.deletedmember.invited,member.role_updatedworkspace.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-Signatureheader - 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
| Type | Description |
|---|---|
text | Free-text input |
number | Numeric input |
date | Date picker |
select | Single-select dropdown |
multi_select | Multi-select dropdown |
url | URL input |
boolean | Toggle/checkbox |
How it works
- OWNER/ADMIN defines custom fields in Settings > Custom Fields
- Each field has a name, type, optional validation, and sort order
- Field values are stored in the
custom_fieldsJSONB column on theproductstable - Custom field definitions are stored in the
custom_field_definitionstable
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:
- Add a
custom_fields JSONB DEFAULT '{}'column to your table - Reuse the
custom_field_definitionstable with a differentresource_type - Use the same
custom-field-actions.tsfunctions 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
| Setting | Options |
|---|---|
| Channel | In-app, Email |
| Frequency | Immediate, 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:
- Define notification types for your features
- Check user preferences before sending
- 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:
- Queries for actions where
scheduled_for <= now()andexecuted_at IS NULL - Processes each action based on its
action_type - Sets
executed_atto 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_allowlisttable - 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):
- On every workspace page load, the layout fetches the workspace's IP allowlist
- The client IP is read from
x-forwarded-forheaders - The IP is checked against all CIDR ranges using
src/lib/ip-allowlist/check-ip.ts - If blocked, an "Access Denied" page is shown instead of the workspace content
- 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
impersonateUserspermission - 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
| Feature | Starter | Pro | Enterprise |
|---|---|---|---|
| API Keys | yes | yes | yes |
| Outgoing Webhooks | yes | yes | yes |
| Custom Fields | yes | yes | yes |
| Bookmarks & Saved Views | yes | yes | yes |
| Notifications | yes | yes | yes |
| Scheduled Actions | yes | yes | yes |
| SSO | -- | -- | yes |
| IP Allowlist | -- | -- | yes |
| Impersonation | -- | -- | yes |