Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .changeset/auto-compile-http-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,24 @@
'@pgflow/edge-worker': minor
---

Add auto-compilation flow and HTTP control plane server for edge worker
## @pgflow/edge-worker

- Add ControlPlane HTTP server for flow compilation (`ControlPlane.serve()`)
- Support namespace imports for flow registration

## @pgflow/cli

### Breaking Changes

- `pgflow compile` now takes flow slug instead of file path
- Compilation happens via HTTP to ControlPlane (local Deno no longer required)
- Deprecate `--deno-json` flag (will be removed in v1.0)

### New Features

- `pgflow install` now scaffolds complete setup:
- Creates `supabase/flows/` with example GreetUser flow
- Creates `supabase/functions/pgflow/` Control Plane
- Creates `supabase/functions/greet-user-worker/` example worker
- Add `--control-plane-url` option to compile command
- Dynamic version injection in generated deno.json files
108 changes: 108 additions & 0 deletions PLAN_auth-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# PLAN: Auth Verification for Control Plane & Workers

**Created**: 2025-11-28
**Status**: Future Work
**Related**: PLAN_control-plane-edge-worker-compilation.md

---

## Goal

Control plane and workers verify the secret key to protect sensitive operations.

## Supabase Key Context

Supabase is transitioning from JWT-based keys to new API keys:

| Old Name | New Name | Format | Use Case |
|----------|----------|--------|----------|
| `anon` | publishable key | `sb_publishable_...` | Client-side (RLS-restricted) |
| `service_role` | secret key | `sb_secret_...` | Server-side (bypasses RLS) |

**Important notes:**
- **CLI/local dev** (`supabase start`): Only has legacy `anon`/`service_role` keys
- **Hosted Supabase**: Has both old and new key formats
- **Edge Functions**: Have `SUPABASE_SERVICE_ROLE_KEY` env var available by default
- **Timeline**: Legacy keys deprecated late 2026
- **Reference**: https://supabase.com/docs/guides/api/api-keys

**Supabase docs explicitly list "periodic jobs, queue processors, topic subscribers" as use cases for secret/service_role keys.**

---

## Why Auth is Required

Both pgflow functions need **service_role/secret key** protection:

| Function | Why Secret Key Required |
|----------|------------------------|
| **Control Plane** (`/pgflow/`) | Enumerate flows, compile flows - dangerous operations |
| **Workers** (`/greet-user-worker/`) | Full DB access, execute arbitrary handlers |

---

## Important: New Keys Require Manual Verification

From Supabase docs:
> "Edge Functions only support JWT verification via the anon and service_role JWT-based API keys. You will need to use the --no-verify-jwt option when using publishable and secret keys. Implement your own apikey-header authorization logic inside the Edge Function code itself."

**Key is sent in `apikey` header (NOT Authorization)** - new secret keys are not JWTs.

---

## Implementation

### Code Changes

**Files to modify:**
- `pkgs/edge-worker/src/ControlPlane.ts` (or equivalent)
- `pkgs/edge-worker/src/EdgeWorker.ts` (or equivalent)

**Implementation:**
```typescript
// At the start of request handling
const apiKey = req.headers.get('apikey');
const expectedKey = Deno.env.get('PGFLOW_SECRET_KEY'); // custom env var set by user

if (!apiKey || apiKey !== expectedKey) {
return new Response(JSON.stringify({ error: 'Invalid or missing secret key' }), {
status: 401,
});
}
```

### User Setup Required

For production with new secret keys:
1. Create secret key in Supabase dashboard
2. Store as Edge Function secret: `PGFLOW_SECRET_KEY`
3. Pass same key to CLI: `pgflow compile --secret-key <key>`

### Documentation

**Add to docs:**
- Auth model explanation (both functions need service_role key)
- How to pass apikey header
- Production deployment considerations

**Example:**
```bash
curl http://localhost:54321/functions/v1/greet-user-worker \
-H "apikey: $PGFLOW_SECRET_KEY"
```

---

## When to Implement

This should be implemented when:
- Flow enumeration is added to control plane
- Other sensitive operations are exposed
- Before production deployments become common

---

## References

- Supabase API Keys docs: https://supabase.com/docs/guides/api/api-keys
- Edge Function secrets: https://supabase.com/docs/guides/functions/secrets
4 changes: 4 additions & 0 deletions PLAN_control-plane-edge-worker-compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ class EdgeWorker {
- Advisory locks in ControlPlane
- Proper error messages with actionable fixes
- Deployment mode detection (dev/prod)
- **Auth verification for ControlPlane endpoints** (see PLAN_auth-verification.md)
- Verify `apikey` header against `PGFLOW_SECRET_KEY` env var
- Required for production deployments
- Protects flow enumeration and compilation endpoints

---

Expand Down
164 changes: 164 additions & 0 deletions PLAN_workers-start-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# PLAN: pgflow workers start CLI Command

**Created**: 2025-11-28
**Status**: Future Work
**Related**: PLAN_auth-verification.md

---

## Goal

Provide a CLI command to start workers with proper authentication, eliminating the need for manual curl commands.

---

## Command Design

```bash
# Uses SUPABASE_SERVICE_ROLE_KEY from env or .env
pgflow workers start greet-user-worker

# Or explicit key
pgflow workers start greet-user-worker --secret-key <key>

# Multiple workers
pgflow workers start greet-user-worker payment-worker
```

---

## Behavior

### Key Resolution

1. Check `--secret-key` flag
2. Check `SUPABASE_SERVICE_ROLE_KEY` env var
3. Check `.env` file in current directory
4. Check `supabase/.env` file
5. Error if no key found

### Request Handling

- Call worker endpoint with proper `apikey` header
- Handle HTTP responses and errors
- Display worker status/logs
- Handle reconnection on disconnect

### URL Resolution

- Default: `http://localhost:54321/functions/v1/<worker-name>`
- Support `--url` flag for custom endpoints
- Support `SUPABASE_URL` env var for hosted

---

## Implementation

### Files to Create/Modify

- `pkgs/cli/src/commands/workers/start.ts` - New command
- `pkgs/cli/src/commands/workers/index.ts` - Command group

### Command Structure

```typescript
import { Command } from 'commander';

export const workersStartCommand = new Command('start')
.description('Start a worker to process tasks')
.argument('<worker...>', 'Worker function name(s)')
.option('--secret-key <key>', 'Service role / secret key')
.option('--url <url>', 'Supabase functions URL')
.action(async (workers, options) => {
const secretKey = resolveSecretKey(options);

for (const worker of workers) {
await startWorker(worker, secretKey, options.url);
}
});

async function startWorker(name: string, secretKey: string, baseUrl?: string) {
const url = `${baseUrl || getDefaultUrl()}/functions/v1/${name}`;

const response = await fetch(url, {
headers: {
'apikey': secretKey,
},
});

// Handle response, reconnection, etc.
}
```

### Key Resolution Function

```typescript
function resolveSecretKey(options: { secretKey?: string }): string {
// 1. CLI flag
if (options.secretKey) return options.secretKey;

// 2. Env var
if (process.env.SUPABASE_SERVICE_ROLE_KEY) {
return process.env.SUPABASE_SERVICE_ROLE_KEY;
}

// 3. .env file
const envPath = findEnvFile();
if (envPath) {
const env = dotenv.parse(fs.readFileSync(envPath));
if (env.SUPABASE_SERVICE_ROLE_KEY) {
return env.SUPABASE_SERVICE_ROLE_KEY;
}
}

throw new Error('No secret key found. Provide --secret-key or set SUPABASE_SERVICE_ROLE_KEY');
}
```

---

## UX Considerations

### Output Format

```
$ pgflow workers start greet-user-worker

Starting worker: greet-user-worker
URL: http://localhost:54321/functions/v1/greet-user-worker
Auth: Using SUPABASE_SERVICE_ROLE_KEY from environment

Worker started successfully. Press Ctrl+C to stop.
```

### Error Messages

```
$ pgflow workers start greet-user-worker

Error: No secret key found.

To fix this, either:
1. Set SUPABASE_SERVICE_ROLE_KEY environment variable
2. Add SUPABASE_SERVICE_ROLE_KEY to your .env file
3. Use --secret-key flag: pgflow workers start greet-user-worker --secret-key <key>
```

---

## When to Implement

After:
- Auth verification is implemented (PLAN_auth-verification.md)
- Basic worker functionality is stable
- User feedback indicates need for easier worker management

---

## Future Enhancements

- `pgflow workers list` - List available workers
- `pgflow workers status` - Show running workers
- `pgflow workers stop` - Stop running workers
- Watch mode for development
- Multiple worker instances with `--concurrency` flag
20 changes: 10 additions & 10 deletions pkgs/cli/__tests__/commands/compile/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('fetchFlowSQL', () => {
const result = await fetchFlowSQL(
'test_flow',
'http://127.0.0.1:50621/functions/v1/pgflow',
'test-publishable-key'
'test-secret-key'
);

expect(result).toEqual({
Expand All @@ -58,8 +58,8 @@ describe('fetchFlowSQL', () => {
'http://127.0.0.1:50621/functions/v1/pgflow/flows/test_flow',
{
headers: {
'Authorization': 'Bearer test-publishable-key',
'apikey': 'test-publishable-key',
'Authorization': 'Bearer test-secret-key',
'apikey': 'test-secret-key',
'Content-Type': 'application/json',
},
}
Expand Down Expand Up @@ -142,7 +142,7 @@ describe('fetchFlowSQL', () => {
const result = await fetchFlowSQL(
'test_flow',
'http://127.0.0.1:50621',
'test-publishable-key'
'test-secret-key'
);

expect(result).toHaveProperty('flowSlug');
Expand Down Expand Up @@ -224,7 +224,7 @@ describe('fetchFlowSQL', () => {
await fetchFlowSQL(
'my_complex_flow_123',
'http://127.0.0.1:50621/functions/v1/pgflow',
'test-publishable-key'
'test-secret-key'
);

expect(global.fetch).toHaveBeenCalledWith(
Expand All @@ -233,7 +233,7 @@ describe('fetchFlowSQL', () => {
);
});

it('should pass publishable key in Authorization header', async () => {
it('should pass secret key in Authorization header', async () => {
const mockResponse = {
ok: true,
status: 200,
Expand All @@ -248,13 +248,13 @@ describe('fetchFlowSQL', () => {
await fetchFlowSQL(
'test_flow',
'http://127.0.0.1:50621/functions/v1/pgflow',
'my-special-publishable-key'
'my-special-secret-key'
);

expect(global.fetch).toHaveBeenCalledWith(expect.any(String), {
headers: {
'Authorization': 'Bearer my-special-publishable-key',
'apikey': 'my-special-publishable-key',
'Authorization': 'Bearer my-special-secret-key',
'apikey': 'my-special-secret-key',
'Content-Type': 'application/json',
},
});
Expand All @@ -276,7 +276,7 @@ describe('fetchFlowSQL', () => {
const result = await fetchFlowSQL(
'empty_flow',
'http://127.0.0.1:50621/functions/v1/pgflow',
'test-publishable-key'
'test-secret-key'
);

expect(result.sql).toEqual([]);
Expand Down
2 changes: 1 addition & 1 deletion pkgs/cli/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"cwd": "{projectRoot}",
"readyWhen": "Serving functions on http://",
"streamOutput": true,
"command": "../../scripts/supabase-start-locked.sh . && ./scripts/sync-e2e-deps.sh && supabase functions serve --import-map supabase/functions/pgflow/deno.json"
"command": "../../scripts/supabase-start-locked.sh . && ./scripts/sync-e2e-deps.sh && supabase functions serve --no-verify-jwt --import-map supabase/functions/pgflow/deno.json"
}
},
"hang": {
Expand Down
Loading