v17.0: Production Readiness - Security & Authorization
Priority: CRITICAL (BLOCKING) Estimated Effort: 2-3 weeks (single developer) Dependencies: None (foundational work) Standards: OWASP API Security Top 10 (2023)
Executive Summary
This release addresses critical security vulnerabilities that must be resolved before public launch. The most severe issue is a horizontal privilege escalation bug (OWASP API1:2023 - Broken Object Level Authorization) allowing any authenticated user to modify or delete any service in the database.
Defense in Depth Strategy:
- Layer 1 (Database): Supabase Row Level Security (RLS) policies - primary defense
- Layer 2 (Application): Authorization utility with ownership checks - defense in depth
- Layer 3 (API Gateway): Rate limiting and input validation - abuse prevention
User Review Required
[!IMPORTANT] BLOCKING SECURITY ISSUE: The current API allows any authenticated user to modify/delete ANY service, not just their own. This is OWASP API1:2023 (Broken Object Level Authorization) - the #1 API security risk.
[!WARNING] Rate Limiting Migration: Moving from in-memory to persistent rate limiting requires either Vercel KV (~$1/mo) or Upstash Redis (free tier). User must approve approach.
[!NOTE] RLS Policies: This plan implements RLS as the primary defense. The v17.2 Dashboard plan will extend these policies for partner-specific features.
Phase 0: Database-Level Security (RLS) - PRIMARY DEFENSE
Goal: Implement Row Level Security as the first line of defense. Even if application code has bugs, the database prevents unauthorized access.
0.1 Enable RLS on Services Table
[SQL] Supabase Migration
-- Enable RLS on services table
ALTER TABLE services ENABLE ROW LEVEL SECURITY;
-- Policy: Anyone can read published services (verification_level > 0)
CREATE POLICY "Public can view published services"
ON services FOR SELECT
USING (verification_level > 0 AND deleted_at IS NULL);
-- Policy: Authenticated users can insert for their organization
CREATE POLICY "Org members can insert services"
ON services FOR INSERT
TO authenticated
WITH CHECK (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
);
-- Policy: Org members can update their org's services
CREATE POLICY "Org members can update own services"
ON services FOR UPDATE
TO authenticated
USING (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin', 'editor')
)
)
WITH CHECK (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin', 'editor')
)
);
-- Policy: Only admins/owners can delete
CREATE POLICY "Org admins can delete own services"
ON services FOR DELETE
TO authenticated
USING (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);
0.2 Create Audit Log Table
[SQL] Supabase Migration
-- Audit log for security-sensitive operations
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT NOW(),
user_id UUID REFERENCES auth.users(id),
action TEXT NOT NULL, -- 'service.create', 'service.update', 'service.delete', 'admin.access'
resource_type TEXT NOT NULL, -- 'service', 'organization', 'user'
resource_id TEXT,
old_values JSONB,
new_values JSONB,
ip_address INET,
user_agent TEXT,
success BOOLEAN DEFAULT true,
error_message TEXT
);
-- Index for querying by user and resource
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id, created_at DESC);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id, created_at DESC);
-- RLS: Users can only see their own audit logs, admins see all
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own audit logs"
ON audit_logs FOR SELECT
TO authenticated
USING (user_id = auth.uid());
0.3 Create Organization Members Table (if not exists)
[SQL] Supabase Migration
-- Organization membership with roles
CREATE TABLE IF NOT EXISTS organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'editor', 'viewer')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(org_id, user_id)
);
-- Enable RLS
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
-- Members can see their own org memberships
CREATE POLICY "Users see own memberships"
ON organization_members FOR SELECT
TO authenticated
USING (user_id = auth.uid());
-- Only org owners/admins can manage members
CREATE POLICY "Org admins manage members"
ON organization_members FOR ALL
TO authenticated
USING (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);
Phase 1: Application-Level Authorization (Defense in Depth)
Goal: Add application-level checks as a second layer of defense. These provide better error messages and logging than RLS alone.
1.1 Create Authorization Utility
[NEW] lib/auth/authorization.ts
Create a centralized authorization utility for ownership checks:
// Proposed interface
export async function assertServiceOwnership(userId: string, serviceId: string): Promise<void>
export async function assertOrganizationMembership(
userId: string,
orgId: string,
requiredRole?: OrganizationRole
): Promise<void>
export async function getEffectivePermissions(
userId: string,
resourceType: "service" | "feedback" | "organization"
): Promise<Permission[]>
- Centralize all ownership/permission checks
- Throw standardized
AuthorizationErroron failure - Support role hierarchy: owner > admin > editor > viewer
1.2 Fix Service Update/Delete Authorization
[MODIFY] app/api/v1/services/[id]/route.ts
Current State (VULNERABLE):
// Line ~45: Only checks if user exists
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) return createApiError("Unauthorized", 401)
// MISSING: Check if user owns this service!
Required Changes:
- Import authorization utility
- Before PUT: Verify user's organization owns the service
- Before DELETE: Verify user's organization owns the service
- Return 403 Forbidden if ownership check fails
Implementation Steps:
- [x] Add
org_idlookup from service record - [x] Query
organization_membersto verify user membership - [x] Check role permits the operation (editor+ for edit, admin+ for delete)
- [x] Add test cases for unauthorized access attempts
1.3 Fix Admin Route Authorization
[MODIFY] app/api/admin/save/route.ts
Current State (WEAK):
if (process.env.NODE_ENV === "production") {
return NextResponse.json({ error: "Not available" }, { status: 403 })
}
Required Changes:
- [x] Check for actual admin role, not just environment
- [x] Query user's role from
organization_membersor dedicatedadminstable - [x] Log admin actions to audit table
[MODIFY] app/api/admin/push/route.ts
- [x] Add admin role verification (currently only checks if user exists)
- [x] Validate user has push notification permissions
[MODIFY] app/api/admin/reindex/route.ts
- [x] Add admin role verification
- [x] Add audit logging for reindex operations
[MODIFY] app/api/admin/data/route.ts
- [x] Add admin role verification
Phase 2: Rate Limiting Persistence
Goal: Rate limiting survives serverless cold starts.
2.1 Evaluate Storage Options
| Option | Pros | Cons | Cost |
|---|---|---|---|
| Vercel KV | Native integration, simple | Vendor lock-in | ~$1/mo for low usage |
| Upstash Redis | Serverless-friendly, free tier | Additional account | Free tier: 10K/day |
| Supabase | Already integrated | Not designed for this | Included |
Recommendation: Upstash Redis (free tier sufficient for pilot)
2.2 Implement Persistent Rate Limiter
[MODIFY] lib/rate-limit.ts
Current State:
const requests = new Map<string, { count: number; resetTime: number }>()
// In-memory only - resets on cold start
Required Changes:
- Create Redis client configuration
- Implement atomic increment with TTL
- Fallback to in-memory if Redis unavailable
- Add connection pooling for performance
[NEW] lib/rate-limit/redis-store.ts
- Redis-backed rate limit store
- Atomic operations using INCR + EXPIRE
- Connection health monitoring
[MODIFY] .env.example
Add Redis configuration:
Phase 3: API Security Hardening
Goal: Secure remaining API endpoints and standardize error handling.
3.1 Secure Service Export Endpoint
[MODIFY] app/api/v1/services/export/route.ts
Current State: Unauthenticated endpoint exposes all service data + embeddings
Required Changes:
- [x] Require authentication for full export
- [x] Create separate public endpoint with limited fields (no embeddings)
- [x] Add rate limiting (stricter than search)
- [x] Log export requests for audit
Alternative: Split into two endpoints:
/api/v1/services/export- Authenticated, full data + embeddings/api/v1/services/public-export- Unauthenticated, limited fields
3.2 Standardize Error Responses
[NEW] lib/api/errors.ts
Create standardized error response factory:
interface ApiErrorResponse {
error: {
code: string // e.g., "UNAUTHORIZED", "FORBIDDEN", "VALIDATION_ERROR"
message: string // Human-readable message
details?: unknown // Validation errors, field-specific info
requestId: string // For log correlation
}
}
[MODIFY] lib/api-utils.ts
- [x] Update
createApiErrorto use new format - [x] Add error code constants
- [x] Ensure all routes use standardized format
Routes to update:
- [x]
app/api/v1/feedback/route.ts- Currently returns{ success: false, message } - [x]
app/api/v1/services/route.ts- Uses correct format - [x]
app/api/v1/search/services/route.ts- Uses correct format - [x] All admin routes
3.3 Add PATCH Endpoint for Partial Updates
[MODIFY] app/api/v1/services/[id]/route.ts
Add PATCH handler:
- [x] Accept partial service object
- [x] Validate only provided fields with Zod
.partial() - [x] Merge with existing service data
- [x] Same authorization checks as PUT
3.4 Implement Soft Deletes
[MODIFY] types/service.ts
Add soft delete field:
[MODIFY] app/api/v1/services/[id]/route.ts
DELETE handler changes:
- [x] Set
deleted_attimestamp instead of hard delete - [x] Record
deleted_byuser ID - [x] Return success with warning about soft delete
[MODIFY] lib/search/data.ts
- [x] Filter out soft-deleted services in all queries
- [x] Add
includeDeletedoption for admin views
Phase 4: Cleanup & Documentation
4.1 Deprecate Legacy Endpoints
[MODIFY] app/api/feedback/route.ts
- [x] Add deprecation warning header
- [x] Redirect to
/api/v1/feedbackwith 301 - [x] Log deprecation usage for monitoring
4.2 Remove Unused Endpoints
[DELETE] app/api/v1/submissions/route.ts
- Mock endpoint never implemented
- Remove file entirely
4.3 Create ADR
[NEW] docs/adr/007-api-authorization.md
Document:
- Authorization model decisions
- Role hierarchy
- Rate limiting strategy
- Error response standardization
Verification Plan
Automated Tests
[NEW] tests/api/authorization.test.ts
Test cases:
- [x] Authenticated user can only modify own organization's services
- [x] Unauthenticated user receives 401 on protected endpoints
- [x] User without ownership receives 403
- [x] Admin can access admin endpoints
- [x] Non-admin receives 403 on admin endpoints
- [x] Rate limiting triggers after threshold
- [x] Soft-deleted services excluded from search
[NEW] tests/lib/rate-limit-redis.test.ts
Test cases:
- [x] Redis store increments correctly
- [x] TTL expires after window
- [x] Fallback to in-memory on Redis failure
- [x] Concurrent requests handled atomically
Manual Verification
- [x] Create two test users in different organizations
- [x] Verify User A cannot modify User B's services
- [x] Verify rate limiting persists across serverless cold starts
- [x] Verify admin endpoints require admin role
- [x] Test soft delete and verify service hidden from search
Security Checklist
- [x] Run
npm audit- zero high/medium vulnerabilities - [x] Test horizontal privilege escalation (should fail)
- [x] Test rate limit bypass attempts
- [x] Verify error responses don't leak sensitive info
- [x] Check audit logs capture admin actions
File Change Summary
| Action | File | Priority |
|---|---|---|
| Database (Phase 0) | ||
| SQL | supabase/migrations/XXX_rls_services.sql | P0 |
| SQL | supabase/migrations/XXX_audit_logs.sql | P0 |
| SQL | supabase/migrations/XXX_org_members.sql | P0 |
| Application (Phase 1) | ||
| NEW | lib/auth/authorization.ts | P0 |
| NEW | lib/auth/audit.ts | P0 |
| MODIFY | app/api/v1/services/[id]/route.ts | P0 |
| MODIFY | app/api/admin/save/route.ts | P0 |
| MODIFY | app/api/admin/push/route.ts | P0 |
| MODIFY | app/api/admin/reindex/route.ts | P1 |
| MODIFY | app/api/admin/data/route.ts | P1 |
| Rate Limiting (Phase 2) | ||
| NEW | lib/rate-limit/redis-store.ts | P1 |
| MODIFY | lib/rate-limit.ts | P1 |
| API Hardening (Phase 3) | ||
| MODIFY | app/api/v1/services/export/route.ts | P1 |
| NEW | lib/api/errors.ts | P2 |
| MODIFY | lib/api-utils.ts | P2 |
| MODIFY | app/api/v1/feedback/route.ts | P2 |
| MODIFY | types/service.ts | P2 |
| MODIFY | lib/search/data.ts | P2 |
| Cleanup (Phase 4) | ||
| MODIFY | app/api/feedback/route.ts | P3 |
| DELETE | app/api/v1/submissions/route.ts | P3 |
| NEW | docs/adr/007-api-authorization.md | P3 |
| Tests | ||
| NEW | tests/api/authorization.test.ts | P0 |
| NEW | tests/api/rls-policies.test.ts | P0 |
| NEW | tests/lib/rate-limit-redis.test.ts | P1 |
Dependencies
- External: Upstash Redis account (free tier) OR Vercel KV
- Internal: None - this is foundational work
Migration Path
- Database first: Run RLS migrations in Supabase (can be done without code deploy)
- Test RLS: Verify policies block unauthorized access in staging
- Deploy app: Authorization utility + audit logging
- Enable rate limiting: After Redis/KV configured
- Cleanup: Deprecation warnings, then removal in next release
Success Criteria
- [x] Zero horizontal privilege escalation possible (verified by RLS + app tests)
- [x] RLS policies active on all sensitive tables
- [x] All API routes have authorization checks (defense in depth)
- [x] Rate limiting survives cold starts (verified by test)
- [x] Error responses follow standardized format
- [x] All admin actions logged to audit table
- [x] Audit logs queryable by user/resource
- [x] ADR-007 documents decisions
Security Monitoring (Post-Deploy)
Recommended Alerts
Set up monitoring for these security events:
| Event | Threshold | Action |
|---|---|---|
| Failed auth attempts | >10/min from same IP | Rate limit, notify |
| RLS policy denials | Any | Log for review |
| Admin endpoint access | Any | Log + notify |
| Rate limit exceeded | >100/hour same user | Investigate |
| Audit log gaps | Missing expected entries | Alert immediately |
Audit Log Review Cadence
- Daily: Review admin actions
- Weekly: Review rate limit violations
- Monthly: Full audit log analysis for anomalies
Rollback Plan
If issues discovered post-deploy:
- RLS breaking app: Temporarily disable specific policy (not all RLS)
- Authorization bugs: Revert to previous route handlers, RLS still protects
- Rate limiting issues: Fallback automatically uses in-memory
- Soft delete problems: Add migration to restore deleted_at column if needed
OWASP Reference
This plan addresses the following OWASP API Security Top 10 (2023) risks:
| Risk | Status | How Addressed |
|---|---|---|
| API1: Broken Object Level Authorization | ✅ Fixed | RLS + ownership checks |
| API2: Broken Authentication | ⬜ Out of scope | Handled by Supabase Auth |
| API4: Unrestricted Resource Consumption | ✅ Fixed | Rate limiting |
| API5: Broken Function Level Authorization | ✅ Fixed | Admin role checks |
| API8: Security Misconfiguration | ✅ Fixed | Standardized errors, no info leakage |