supabase-stateful 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/cli.js +59 -0
- package/package.json +34 -0
- package/src/commands/export.js +34 -0
- package/src/commands/init.js +89 -0
- package/src/commands/setup.js +622 -0
- package/src/commands/start.js +178 -0
- package/src/commands/status.js +65 -0
- package/src/commands/stop.js +50 -0
- package/src/commands/sync.js +63 -0
- package/src/lib/cloud.js +169 -0
- package/src/lib/config.js +75 -0
- package/src/lib/docker.js +96 -0
- package/src/lib/state.js +204 -0
- package/src/utils/log.js +21 -0
- package/src/utils/prompt.js +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anthony Grant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# supabase-stateful
|
|
2
|
+
|
|
3
|
+
> Persistent local state for Supabase development
|
|
4
|
+
|
|
5
|
+
**Note:** Currently supports Next.js (App Router) only. The core state persistence works with any framework, but the generated client files are Next.js specific.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Local Supabase is **stateless by default**. Stop it, lose everything. Restart, get `duplicate key` errors.
|
|
10
|
+
|
|
11
|
+
## The Solution
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx supabase-stateful setup # Interactive setup
|
|
15
|
+
|
|
16
|
+
npm run supabase:start # Restores your previous session
|
|
17
|
+
# ... develop, create test users, add data ...
|
|
18
|
+
npm run supabase:stop # Saves state for next time
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Next time you start, **everything is back** - test users, data, relationships intact.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
**Prerequisites:** [Supabase CLI](https://supabase.com/docs/guides/cli) and [Docker](https://docs.docker.com/desktop)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Have a Supabase project (run `supabase init` if you don't)
|
|
29
|
+
|
|
30
|
+
# 2. Run interactive setup
|
|
31
|
+
npx supabase-stateful setup
|
|
32
|
+
|
|
33
|
+
# 3. Start developing
|
|
34
|
+
npm run dev:local # or ./scripts/dev-local.sh for graceful shutdown
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The setup wizard will:
|
|
38
|
+
- Install required dependencies (`@supabase/ssr`, `@supabase/supabase-js`, `concurrently`)
|
|
39
|
+
- Create Supabase client files with local/production switching (Next.js only)
|
|
40
|
+
- Add npm scripts for stateful start/stop
|
|
41
|
+
- Generate a graceful shutdown script (Ctrl+C saves state automatically)
|
|
42
|
+
- Optionally install GitHub Actions for CI/CD migrations
|
|
43
|
+
|
|
44
|
+
## Daily Workflow
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run dev:local # Start with local Supabase (restores your data)
|
|
48
|
+
npm run supabase:stop # Save state and stop (or Ctrl+C with dev-local.sh)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
| On Stop | On Start |
|
|
54
|
+
|---------|----------|
|
|
55
|
+
| Export database state | Start Supabase |
|
|
56
|
+
| Clear auth tokens | Restore saved state |
|
|
57
|
+
| Stop Supabase | Apply pending migrations on top |
|
|
58
|
+
|
|
59
|
+
Migrations run **on top of your existing data**, not on an empty database.
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
| Command | Description |
|
|
64
|
+
|---------|-------------|
|
|
65
|
+
| `setup` | Interactive setup wizard |
|
|
66
|
+
| `start` | Start and restore state |
|
|
67
|
+
| `stop` | Save state and stop |
|
|
68
|
+
| `status` | Show current status |
|
|
69
|
+
|
|
70
|
+
## Deployment
|
|
71
|
+
|
|
72
|
+
The setup wizard can install a GitHub Actions workflow for CI/CD:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Push to main → Run migrations on production → Deploy app
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
1. **Migrations job** - Applies your local `supabase/migrations/*.sql` files to your production Supabase database
|
|
79
|
+
2. **Deploy job** - Builds and deploys your app to Vercel (optional)
|
|
80
|
+
|
|
81
|
+
See [CI/CD with GitHub Actions](docs/github-actions.md) for setup details and required secrets.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- [New Project Setup](docs/new-project.md)
|
|
86
|
+
- [Existing Project Setup](docs/existing-project.md)
|
|
87
|
+
- [CI/CD with GitHub Actions](docs/github-actions.md)
|
|
88
|
+
- [Troubleshooting](docs/troubleshooting.md)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { init } from '../src/commands/init.js';
|
|
5
|
+
import { setup } from '../src/commands/setup.js';
|
|
6
|
+
import { start } from '../src/commands/start.js';
|
|
7
|
+
import { stop } from '../src/commands/stop.js';
|
|
8
|
+
import { status } from '../src/commands/status.js';
|
|
9
|
+
import { sync } from '../src/commands/sync.js';
|
|
10
|
+
import { exportData } from '../src/commands/export.js';
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('supabase-stateful')
|
|
14
|
+
.description('Persistent local state for Supabase development')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Initialize supabase-stateful (basic - just adds npm scripts)')
|
|
20
|
+
.action(init);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('setup')
|
|
24
|
+
.description('Full setup - creates Supabase client files + npm scripts')
|
|
25
|
+
.option('--force', 'Overwrite existing files')
|
|
26
|
+
.option('-y, --yes', 'Skip interactive prompts (auto-confirm defaults)')
|
|
27
|
+
.action(setup);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('start')
|
|
31
|
+
.description('Start Supabase and restore saved state')
|
|
32
|
+
.action(start);
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('stop')
|
|
36
|
+
.description('Save state, clear auth tokens, and stop Supabase')
|
|
37
|
+
.action(stop);
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('status')
|
|
41
|
+
.description('Show current status')
|
|
42
|
+
.action(status);
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command('sync')
|
|
46
|
+
.description('Sync cloud data to local database')
|
|
47
|
+
.option('--sample', 'Limit to 100 rows per table')
|
|
48
|
+
.option('--tables <tables>', 'Comma-separated list of tables')
|
|
49
|
+
.action(sync);
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('export')
|
|
53
|
+
.description('Export cloud data to seed file')
|
|
54
|
+
.option('--sample', 'Limit to 100 rows per table')
|
|
55
|
+
.option('--tables <tables>', 'Comma-separated list of tables')
|
|
56
|
+
.option('--output <path>', 'Output file path')
|
|
57
|
+
.action(exportData);
|
|
58
|
+
|
|
59
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "supabase-stateful",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Persistent local state for Supabase development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"supabase-stateful": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"supabase",
|
|
15
|
+
"local",
|
|
16
|
+
"development",
|
|
17
|
+
"database",
|
|
18
|
+
"state",
|
|
19
|
+
"persistence"
|
|
20
|
+
],
|
|
21
|
+
"author": "Anthony Grant",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/agrant2711/supabase-stateful"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.0.0",
|
|
32
|
+
"toml": "^3.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export command - export cloud data to a seed file
|
|
3
|
+
*
|
|
4
|
+
* Options:
|
|
5
|
+
* --sample Limit to 100 rows per table
|
|
6
|
+
* --tables Comma-separated list of tables
|
|
7
|
+
* --output Output file path (default: supabase/seed-data.sql)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { exportCloudData } from '../lib/cloud.js';
|
|
11
|
+
import { log } from '../utils/log.js';
|
|
12
|
+
|
|
13
|
+
export async function exportData(options) {
|
|
14
|
+
log.info('Exporting cloud data...');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const output = await exportCloudData({
|
|
18
|
+
sample: options.sample,
|
|
19
|
+
tables: options.tables,
|
|
20
|
+
output: options.output || 'supabase/seed-data.sql',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.log('');
|
|
24
|
+
log.success('Export complete!');
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log('Next steps:');
|
|
27
|
+
console.log(' 1. Review the generated seed file');
|
|
28
|
+
console.log(' 2. Run: supabase-stateful sync (to apply to local)');
|
|
29
|
+
console.log(` Or manually: supabase db reset && psql < ${output}`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log.error(`Export failed: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - set up supabase-stateful in a project
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Check for supabase/config.toml (must have run 'supabase init' first)
|
|
6
|
+
* 2. Parse project name from config
|
|
7
|
+
* 3. Create .supabase-stateful.json with container name
|
|
8
|
+
* 4. Add npm scripts to package.json
|
|
9
|
+
* 5. Add state file to .gitignore
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import toml from 'toml';
|
|
15
|
+
import { log } from '../utils/log.js';
|
|
16
|
+
import { saveConfig, fileExists, appendIfMissing, configExists } from '../lib/config.js';
|
|
17
|
+
|
|
18
|
+
export async function init() {
|
|
19
|
+
log.info('Initializing supabase-stateful...');
|
|
20
|
+
|
|
21
|
+
// Check if already initialized
|
|
22
|
+
if (await configExists()) {
|
|
23
|
+
log.warn('Already initialized (.supabase-stateful.json exists)');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for supabase project
|
|
28
|
+
const supabaseConfigPath = 'supabase/config.toml';
|
|
29
|
+
if (!await fileExists(supabaseConfigPath)) {
|
|
30
|
+
log.error('No supabase/config.toml found');
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log('Run "supabase init" first to create a Supabase project');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse project name from supabase config
|
|
37
|
+
let projectName;
|
|
38
|
+
try {
|
|
39
|
+
const configContent = await fs.readFile(supabaseConfigPath, 'utf8');
|
|
40
|
+
const config = toml.parse(configContent);
|
|
41
|
+
projectName = config.project_id || path.basename(process.cwd());
|
|
42
|
+
} catch {
|
|
43
|
+
projectName = path.basename(process.cwd());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log.info(`Detected project: ${projectName}`);
|
|
47
|
+
|
|
48
|
+
// Create config file
|
|
49
|
+
const statefulConfig = {
|
|
50
|
+
stateFile: 'supabase/local-state.sql',
|
|
51
|
+
containerName: `supabase_db_${projectName}`,
|
|
52
|
+
};
|
|
53
|
+
await saveConfig(statefulConfig);
|
|
54
|
+
log.success('Created .supabase-stateful.json');
|
|
55
|
+
|
|
56
|
+
// Update package.json with npm scripts
|
|
57
|
+
if (await fileExists('package.json')) {
|
|
58
|
+
try {
|
|
59
|
+
const pkgContent = await fs.readFile('package.json', 'utf8');
|
|
60
|
+
const pkg = JSON.parse(pkgContent);
|
|
61
|
+
|
|
62
|
+
pkg.scripts = pkg.scripts || {};
|
|
63
|
+
pkg.scripts['supabase:start'] = 'supabase-stateful start';
|
|
64
|
+
pkg.scripts['supabase:stop'] = 'supabase-stateful stop';
|
|
65
|
+
pkg.scripts['supabase:status'] = 'supabase-stateful status';
|
|
66
|
+
|
|
67
|
+
await fs.writeFile('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
68
|
+
log.success('Added npm scripts to package.json');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.warn(`Could not update package.json: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add state file to .gitignore
|
|
75
|
+
await appendIfMissing('.gitignore', 'supabase/local-state.sql');
|
|
76
|
+
log.success('Added state file to .gitignore');
|
|
77
|
+
|
|
78
|
+
console.log('');
|
|
79
|
+
log.success('Initialized!');
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log('Usage:');
|
|
82
|
+
console.log(' npm run supabase:start Start and restore saved state');
|
|
83
|
+
console.log(' npm run supabase:stop Save state and stop');
|
|
84
|
+
console.log(' npm run supabase:status Show current status');
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log('Or run directly:');
|
|
87
|
+
console.log(' npx supabase-stateful start');
|
|
88
|
+
console.log(' npx supabase-stateful stop');
|
|
89
|
+
}
|