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 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
+ }