auto-claude: subtask-1-2 - Apply migration to Supabase database
Created migration application scripts and comprehensive documentation: - scripts/apply-migration.js - Automated migration (requires service role key) - scripts/verify-migration.js - Table verification script - scripts/test-env.js - Environment diagnostics - supabase/APPLY_MIGRATION.md - Comprehensive migration guide - supabase/MIGRATION_INSTRUCTIONS.md - Quick reference Manual application via Supabase dashboard is recommended approach. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"active": true,
|
"active": true,
|
||||||
"spec": "017-replace-in-memory-rate-limiter-with-persistent-sol",
|
"spec": "017-replace-in-memory-rate-limiter-with-persistent-sol",
|
||||||
"state": "planning",
|
"state": "building",
|
||||||
"subtasks": {
|
"subtasks": {
|
||||||
"completed": 0,
|
"completed": 1,
|
||||||
"total": 0,
|
"total": 12,
|
||||||
"in_progress": 1,
|
"in_progress": 1,
|
||||||
"failed": 0
|
"failed": 0
|
||||||
},
|
},
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"max": 1
|
"max": 1
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"number": 2,
|
"number": 3,
|
||||||
"started_at": "2026-01-25T06:23:53.972774"
|
"started_at": "2026-01-25T06:23:53.972774"
|
||||||
},
|
},
|
||||||
"last_update": "2026-01-25T06:29:27.619539"
|
"last_update": "2026-01-25T06:31:44.749574"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to apply Supabase migration
|
||||||
|
*
|
||||||
|
* This script applies the rate_limits table migration to Supabase.
|
||||||
|
*
|
||||||
|
* IMPORTANT: This script requires SUPABASE_SERVICE_ROLE_KEY to be set
|
||||||
|
* in your .env.local file to execute DDL statements.
|
||||||
|
*
|
||||||
|
* If you don't have the service role key, you can apply the migration manually:
|
||||||
|
* 1. Go to https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
|
||||||
|
* 2. Copy the contents of supabase/migrations/20260125_create_rate_limits_table.sql
|
||||||
|
* 3. Paste into the SQL editor
|
||||||
|
* 4. Click "Run" to execute
|
||||||
|
* 5. Verify the table was created by clicking "Table Editor" and checking for "rate_limits"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
// Load .env.local manually
|
||||||
|
const envPath = path.join(__dirname, '..', '.env.local');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
envContent.split('\n').forEach(line => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const match = trimmed.match(/^([^=:#]+)=(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
const key = match[1].trim();
|
||||||
|
const value = match[2].trim();
|
||||||
|
if (!process.env[key]) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyMigration() {
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl) {
|
||||||
|
console.error('❌ Error: NEXT_PUBLIC_SUPABASE_URL not found in .env.local');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serviceRoleKey) {
|
||||||
|
console.error('❌ Error: SUPABASE_SERVICE_ROLE_KEY not found in .env.local');
|
||||||
|
console.error('\n📝 Manual Migration Instructions:');
|
||||||
|
console.error(' 1. Go to https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql');
|
||||||
|
console.error(' 2. Open: supabase/migrations/20260125_create_rate_limits_table.sql');
|
||||||
|
console.error(' 3. Copy the SQL content and paste into the SQL editor');
|
||||||
|
console.error(' 4. Click "Run" to execute the migration');
|
||||||
|
console.error(' 5. Verify the "rate_limits" table appears in Table Editor\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 Applying migration to Supabase...\n');
|
||||||
|
|
||||||
|
// Create Supabase client with service role key
|
||||||
|
const supabase = createClient(supabaseUrl, serviceRoleKey, {
|
||||||
|
auth: {
|
||||||
|
autoRefreshToken: false,
|
||||||
|
persistSession: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read migration file
|
||||||
|
const migrationPath = path.join(__dirname, '..', 'supabase', 'migrations', '20260125_create_rate_limits_table.sql');
|
||||||
|
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
console.log('📄 Migration file loaded:', migrationPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the migration
|
||||||
|
const { data, error } = await supabase.rpc('exec_sql', { sql: migrationSQL });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// If exec_sql RPC doesn't exist, try direct SQL execution
|
||||||
|
console.log('⚠️ exec_sql RPC not found, attempting direct SQL execution...\n');
|
||||||
|
|
||||||
|
// Split the SQL into individual statements
|
||||||
|
const statements = migrationSQL
|
||||||
|
.split(';')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0 && !s.startsWith('--'));
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
const { error: execError } = await supabase.from('_sql').select().sql(statement);
|
||||||
|
if (execError) {
|
||||||
|
throw execError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Migration applied successfully!\n');
|
||||||
|
|
||||||
|
// Verify the table was created
|
||||||
|
console.log('🔍 Verifying table creation...');
|
||||||
|
const { data: tableData, error: tableError } = await supabase
|
||||||
|
.from('rate_limits')
|
||||||
|
.select('*')
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (tableError && tableError.code !== 'PGRST116') { // PGRST116 = no rows found, which is OK
|
||||||
|
throw tableError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Table "rate_limits" verified successfully!\n');
|
||||||
|
console.log('📊 Migration complete. You can view the table at:');
|
||||||
|
console.log(` https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor/rate_limits\n`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error applying migration:', error.message);
|
||||||
|
console.error('\n📝 Please apply the migration manually:');
|
||||||
|
console.error(' 1. Go to https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql');
|
||||||
|
console.error(' 2. Copy the contents of supabase/migrations/20260125_create_rate_limits_table.sql');
|
||||||
|
console.error(' 3. Paste into the SQL editor and click "Run"\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMigration();
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load .env.local manually
|
||||||
|
const envPath = path.join(__dirname, '..', '.env.local');
|
||||||
|
console.log('Loading from:', envPath);
|
||||||
|
console.log('File exists:', fs.existsSync(envPath));
|
||||||
|
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
console.log('\nFile content length:', envContent.length);
|
||||||
|
console.log('First 200 chars:', envContent.substring(0, 200));
|
||||||
|
|
||||||
|
envContent.split('\n').forEach((line, idx) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
console.log(`Line ${idx}: "${trimmed.substring(0, 50)}"`);
|
||||||
|
const match = trimmed.match(/^([^=:#]+)=(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
const key = match[1].trim();
|
||||||
|
const value = match[2].trim();
|
||||||
|
console.log(` -> Matched: ${key} = ${value.substring(0, 20)}...`);
|
||||||
|
if (!process.env[key]) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
} else if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
console.log(' -> No match!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nFinal env vars:');
|
||||||
|
console.log('NEXT_PUBLIC_SUPABASE_URL:', process.env.NEXT_PUBLIC_SUPABASE_URL);
|
||||||
|
console.log('NEXT_PUBLIC_SUPABASE_ANON_KEY:', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ? 'SET' : 'NOT SET');
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to verify the rate_limits table exists in Supabase
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load .env.local manually
|
||||||
|
const envPath = path.join(__dirname, '..', '.env.local');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
envContent.split('\n').forEach(line => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const match = trimmed.match(/^([^=:#]+)=(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
const key = match[1].trim();
|
||||||
|
const value = match[2].trim();
|
||||||
|
if (!process.env[key]) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyMigration() {
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
console.error('❌ Error: Missing Supabase credentials in .env.local');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Verifying rate_limits table in Supabase...\n');
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to query the rate_limits table
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('rate_limits')
|
||||||
|
.select('*')
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.code === '42P01') {
|
||||||
|
console.error('❌ Table "rate_limits" does not exist yet.\n');
|
||||||
|
console.error('📝 Please apply the migration manually:');
|
||||||
|
console.error(' 1. Go to https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql');
|
||||||
|
console.error(' 2. Copy the contents of supabase/migrations/20260125_create_rate_limits_table.sql');
|
||||||
|
console.error(' 3. Paste into the SQL editor and click "Run"');
|
||||||
|
console.error(' 4. Run this script again to verify\n');
|
||||||
|
process.exit(1);
|
||||||
|
} else if (error.code === 'PGRST301' || error.message.includes('JWT')) {
|
||||||
|
console.error('❌ Authentication error. Table may exist but is not accessible with anon key.\n');
|
||||||
|
console.error('📝 Please verify manually:');
|
||||||
|
console.error(' 1. Go to https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor');
|
||||||
|
console.error(' 2. Check if "rate_limits" table appears in the table list\n');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Table "rate_limits" exists and is accessible!');
|
||||||
|
console.log(`📊 Current records: ${data ? data.length : 0}\n`);
|
||||||
|
|
||||||
|
// Try to get table schema info
|
||||||
|
console.log('🔍 Verifying table schema...');
|
||||||
|
const { data: schemaData, error: schemaError } = await supabase
|
||||||
|
.from('rate_limits')
|
||||||
|
.select('identifier, count, window_start, created_at, updated_at')
|
||||||
|
.limit(0);
|
||||||
|
|
||||||
|
if (schemaError && !schemaError.message.includes('no rows')) {
|
||||||
|
console.warn('⚠️ Could not verify all columns:', schemaError.message);
|
||||||
|
} else {
|
||||||
|
console.log('✅ All expected columns are present!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Migration verification successful!');
|
||||||
|
console.log('📊 View table at: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor/rate_limits\n');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during verification:', error.message);
|
||||||
|
console.error('\nDetails:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyMigration();
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# How to Apply the Rate Limits Migration
|
||||||
|
|
||||||
|
## Quick Start (Manual Application)
|
||||||
|
|
||||||
|
Since the Supabase CLI is not configured for this project, please follow these steps to apply the migration manually through the Supabase dashboard:
|
||||||
|
|
||||||
|
### Step 1: Access the SQL Editor
|
||||||
|
|
||||||
|
Open the Supabase SQL Editor in your browser:
|
||||||
|
```
|
||||||
|
https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Copy the Migration SQL
|
||||||
|
|
||||||
|
Open the migration file:
|
||||||
|
```
|
||||||
|
supabase/migrations/20260125_create_rate_limits_table.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the entire contents (approximately 42 lines of SQL).
|
||||||
|
|
||||||
|
### Step 3: Execute the Migration
|
||||||
|
|
||||||
|
1. In the SQL Editor, paste the copied SQL
|
||||||
|
2. Click the **"RUN"** button (or press `Ctrl+Enter` / `Cmd+Enter`)
|
||||||
|
3. Wait for the success message: "Success. No rows returned"
|
||||||
|
|
||||||
|
### Step 4: Verify the Table Was Created
|
||||||
|
|
||||||
|
**Option A: Using Table Editor**
|
||||||
|
1. Navigate to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
|
||||||
|
2. Look for **"rate_limits"** in the table list (left sidebar)
|
||||||
|
3. Click on it to see the table structure
|
||||||
|
|
||||||
|
**Option B: Using SQL Query**
|
||||||
|
Run this query in the SQL Editor:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rate_limits'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see 5 rows (one for each column):
|
||||||
|
- identifier (text)
|
||||||
|
- count (integer)
|
||||||
|
- window_start (timestamp with time zone)
|
||||||
|
- created_at (timestamp with time zone)
|
||||||
|
- updated_at (timestamp with time zone)
|
||||||
|
|
||||||
|
**Option C: Check Indexes**
|
||||||
|
Run this query to verify indexes were created:
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
indexname,
|
||||||
|
indexdef
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE tablename = 'rate_limits';
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
- Primary key on (identifier, window_start)
|
||||||
|
- idx_rate_limits_identifier_window
|
||||||
|
- idx_rate_limits_window_start
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: "relation 'rate_limits' already exists"
|
||||||
|
|
||||||
|
The table is already created! No action needed. Verify it exists using the verification steps above.
|
||||||
|
|
||||||
|
### Error: "permission denied"
|
||||||
|
|
||||||
|
Make sure you're:
|
||||||
|
1. Logged into the correct Supabase account
|
||||||
|
2. Have admin/owner permissions on the project
|
||||||
|
3. Using the correct project URL
|
||||||
|
|
||||||
|
### Table not visible in Table Editor
|
||||||
|
|
||||||
|
1. Refresh the page (F5)
|
||||||
|
2. Check the SQL output for error messages
|
||||||
|
3. Try the SQL verification query in Option B above
|
||||||
|
|
||||||
|
### Need to Revert/Drop the Table?
|
||||||
|
|
||||||
|
If you need to start over, run this SQL:
|
||||||
|
```sql
|
||||||
|
DROP TABLE IF EXISTS rate_limits CASCADE;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-apply the migration from Step 1.
|
||||||
|
|
||||||
|
## Alternative: Automated Scripts
|
||||||
|
|
||||||
|
We've provided scripts for automated migration, but they require additional setup:
|
||||||
|
|
||||||
|
### Using apply-migration.js (requires service role key)
|
||||||
|
|
||||||
|
1. Add `SUPABASE_SERVICE_ROLE_KEY` to `.env.local`
|
||||||
|
2. Run: `node scripts/apply-migration.js`
|
||||||
|
|
||||||
|
### Using Supabase CLI (if installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase migration up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After the migration is applied successfully:
|
||||||
|
1. ✅ Verify the table exists (see Step 4 above)
|
||||||
|
2. ✅ Mark this subtask as complete
|
||||||
|
3. ✅ Proceed to Phase 2: Building the rate limiting utility
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Supabase Migration Instructions
|
||||||
|
|
||||||
|
## Apply Migration: Create rate_limits Table
|
||||||
|
|
||||||
|
### Quick Steps
|
||||||
|
|
||||||
|
1. **Open Supabase SQL Editor**
|
||||||
|
- Go to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
|
||||||
|
|
||||||
|
2. **Copy Migration SQL**
|
||||||
|
- Open: `supabase/migrations/20260125_create_rate_limits_table.sql`
|
||||||
|
- Copy all contents (Ctrl+A, Ctrl+C)
|
||||||
|
|
||||||
|
3. **Execute Migration**
|
||||||
|
- Paste the SQL into the Supabase SQL editor
|
||||||
|
- Click the "Run" button (or press Ctrl+Enter)
|
||||||
|
- Wait for "Success. No rows returned" message
|
||||||
|
|
||||||
|
4. **Verify Table Creation**
|
||||||
|
- Go to Table Editor: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
|
||||||
|
- Look for "rate_limits" table in the list
|
||||||
|
- Click on it to verify the schema
|
||||||
|
|
||||||
|
### Expected Table Schema
|
||||||
|
|
||||||
|
The `rate_limits` table should have:
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| identifier | TEXT | Client identifier (IP address) |
|
||||||
|
| count | INTEGER | Number of requests in window |
|
||||||
|
| window_start | TIMESTAMPTZ | Start of rate limit window |
|
||||||
|
| created_at | TIMESTAMPTZ | When record was created |
|
||||||
|
| updated_at | TIMESTAMPTZ | When record was last updated |
|
||||||
|
|
||||||
|
**Primary Key:** (identifier, window_start)
|
||||||
|
|
||||||
|
**Indexes:**
|
||||||
|
- `idx_rate_limits_identifier_window` on (identifier, window_start DESC)
|
||||||
|
- `idx_rate_limits_window_start` on (window_start)
|
||||||
|
|
||||||
|
### Alternative: Using Supabase CLI (if available)
|
||||||
|
|
||||||
|
If you have Supabase CLI installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase db push
|
||||||
|
```
|
||||||
|
|
||||||
|
Or apply the specific migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
supabase migration up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Query
|
||||||
|
|
||||||
|
Run this query in the SQL editor to verify the table exists:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rate_limits'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see all 5 columns listed.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Error: "relation already exists"**
|
||||||
|
- The table already exists. You can verify by checking Table Editor.
|
||||||
|
|
||||||
|
**Error: "permission denied"**
|
||||||
|
- Make sure you're logged into the correct Supabase project.
|
||||||
|
- Verify you have admin/owner access to the project.
|
||||||
|
|
||||||
|
**Table not appearing in Table Editor**
|
||||||
|
- Refresh the page (F5)
|
||||||
|
- Check the SQL editor for any error messages
|
||||||
|
- Verify the SQL was executed successfully
|
||||||
Reference in New Issue
Block a user