Skip to content
Open
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
11 changes: 8 additions & 3 deletions _bmad-output/epic-execution-state.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ stories:
dependsOn: []
- id: "3.2"
title: "Create Fly deployment template"
status: pending
status: completed
currentPhase: ""
branch: ""
pr: null
branch: "feat/3.2-fly-deployment-template"
pr: 87
dependsOn: []
skippedIssues: []
completedAt: "2026-03-10"
summary:
total: 2
completed: 2
skipped: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Story 3.2: Create Fly deployment template

Status: review

## Story

As a developer,
I want to deploy zopp-server on Fly using a provided template,
so that my team has a shared server without managing infrastructure.

## Acceptance Criteria

1. **Given** the deployment template exists at `deploy/fly/fly.toml`, **When** the user runs `fly launch` with the template, **Then** Fly provisions the app with the correct configuration.

2. **Given** the template configuration, **When** the server starts, **Then** PostgreSQL is configured as the database backend (not SQLite) **And** TLS is handled automatically by Fly's platform **And** a health check endpoint is configured at `/healthz` on port 8080.

3. **Given** the server is deployed and running, **When** the user runs `fly ssh console -C "zopp-server invite create"`, **Then** an invite token is generated that can be shared with team members.

4. **Given** the `deploy/fly/README.md` exists, **When** a user reads it, **Then** it contains step-by-step deployment instructions **And** it documents required and optional environment variables **And** it explains how to generate the first invite token **And** it explains how to connect the CLI to the deployed server.

## Tasks / Subtasks

- [x] Task 1: Create Fly deployment template (AC: 1, 2)
- [x] 1.1 Create `deploy/fly/fly.toml` with app configuration
- [x] 1.2 Configure PostgreSQL as attached database (Fly Postgres)
- [x] 1.3 Configure health check using `/healthz` on port 8080
- [x] 1.4 Configure gRPC service on internal port 50051
- [x] 1.5 Set appropriate VM size and auto-stop settings

- [x] Task 2: Create deployment README (AC: 3, 4)
- [x] 2.1 Create `deploy/fly/README.md` with step-by-step instructions
- [x] 2.2 Document `fly launch` and `fly deploy` workflow
- [x] 2.3 Document Fly Postgres setup (`fly postgres create` + `fly postgres attach`)
- [x] 2.4 Document environment variables (DATABASE_URL, email config, etc.)
- [x] 2.5 Document invite token generation via `fly ssh console`
- [x] 2.6 Document connecting CLI to deployed server
- [x] 2.7 Document TLS (automatic via Fly's edge, gRPC uses h2c internally)

- [x] Task 3: Validation
- [x] 3.1 Verify fly.toml passes `fly launch --config deploy/fly/fly.toml` syntax check (dry review)
- [x] 3.2 Ensure README instructions are consistent with fly.toml settings

## Dev Notes

### Fly Platform Details

**Fly Machines Configuration:**
- Fly uses `fly.toml` for app configuration
- Apps are deployed as Machines (micro VMs)
- Fly handles TLS termination at the edge — the app runs plain gRPC (h2c) internally
- Fly Postgres is a managed Postgres service attached to apps

**Key fly.toml Sections:**
```toml
app = "zopp-server" # User will change this during fly launch

[build]
dockerfile = "server.Dockerfile"

[env]
# Non-secret environment variables
ZOPP_EMAIL_VERIFICATION_REQUIRED = "false"

[http_service]
internal_port = 50051
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

[[services]]
protocol = "tcp"
internal_port = 50051

[[services.ports]]
port = 443
handlers = ["tls", "http"]

[[services.ports]]
port = 80
handlers = ["http"]

[checks]
[checks.health]
port = 8080
type = "http"
interval = "10s"
timeout = "2s"
path = "/healthz"
method = "GET"

[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
```

**Database Configuration:**
- Users create a Fly Postgres cluster: `fly postgres create --name zopp-db`
- Attach to app: `fly postgres attach zopp-db --app zopp-server`
- This automatically sets `DATABASE_URL` as a secret on the app
- zopp-server detects the postgres:// URL and uses PostgreSQL backend

**Health Checks:**
- zopp-server serves HTTP health on port 8080 by default (`--health-addr 0.0.0.0:8080`)
- Liveness: `GET /healthz` → 200 "ok"
- Readiness: `GET /readyz` → 200 when gRPC ready, 503 otherwise
- Fly uses checks to determine if a Machine is healthy

**TLS:**
- Fly automatically terminates TLS at the edge
- Internal traffic within Fly's network is h2c (HTTP/2 cleartext)
- No need for `--tls-cert` / `--tls-key` flags on the server
- CLI connects to `https://<app>.fly.dev:443` — Fly routes to internal port 50051

**Invite Token Generation:**
- Users run: `fly ssh console -C "/usr/local/bin/zopp-server invite create"`
- The zopp-server binary is at `/usr/local/bin/zopp-server` in the Docker image
- This outputs an invite token that can be shared
- Note: The `invite create` subcommand on the server binary creates a bootstrap invite

**CLI Connection:**
- After deployment, the server is available at `https://<appname>.fly.dev`
- Users configure their CLI: `zopp join <invite-token> <email> --server https://<appname>.fly.dev`

### Environment Variables Reference

**Required (set automatically by Fly Postgres attach):**
- `DATABASE_URL` — PostgreSQL connection string

**Optional:**
- `ZOPP_EMAIL_VERIFICATION_REQUIRED` — "true"/"false" (default "true")
- `ZOPP_EMAIL_PROVIDER` — "resend" or "smtp"
- `ZOPP_EMAIL_FROM` — Sender email address
- `RESEND_API_KEY` — If using Resend email provider
- SMTP variables if using SMTP

### Architecture Notes

- This story is purely configuration/documentation — no Rust code changes
- The existing `server.Dockerfile` is used as-is (Fly builds from Dockerfile)
- The `deploy/` directory is new; this is the first deployment template
- Follow similar patterns to the Helm chart in `charts/zopp/` for reference on configuration values

### Project Structure Notes

- New directory: `deploy/fly/`
- Files: `deploy/fly/fly.toml`, `deploy/fly/README.md`
- No changes to existing source code
- No Cargo.toml changes

### References

- [Source: server.Dockerfile] — Docker build for zopp-server
- [Source: apps/zopp-server/src/main.rs] — Server startup, health check endpoints, CLI args
- [Source: charts/zopp/values.yaml] — Helm chart defaults (reference for env vars)
- [Source: CLAUDE.md] — Server commands, TLS configuration, DATABASE_URL usage

### Pre-Submission Checklist

Before submitting a PR, verify each item relevant to your story's scope.

**Documentation:**

- [ ] fly.toml uses correct ports (50051 gRPC, 8080 health)
- [ ] README instructions are accurate and complete
- [ ] No secrets or credentials appear in template files
- [ ] Environment variable documentation matches server implementation

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6

### Debug Log References

### Completion Notes List

- Created fly.toml with gRPC on port 50051, health checks on port 8080
- PostgreSQL via Fly Postgres attach (sets DATABASE_URL automatically)
- TLS handled by Fly edge — no server-side TLS config needed
- README covers full deployment workflow: launch, postgres, deploy, invite, connect CLI
- Email verification disabled by default in template (enable after configuring email provider)

### File List

- deploy/fly/fly.toml (new) — Fly deployment configuration
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
- deploy/fly/README.md (new) — Step-by-step deployment instructions
4 changes: 2 additions & 2 deletions _bmad-output/implementation-artifacts/sprint-status.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ development_status:
epic-2-retrospective: done

# Epic 3: Fly Integration (Sync & Deployment) (Phase 1)
epic-3: in-progress
epic-3: done
3-1-implement-fly-sync-target-and-cli-commands: done
3-2-create-fly-deployment-template: backlog
3-2-create-fly-deployment-template: done
epic-3-retrospective: optional

# Epic 4: GCP Secret Manager Integration (Phase 2)
Expand Down
154 changes: 154 additions & 0 deletions deploy/fly/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Deploy zopp-server on Fly

Deploy a zopp-server instance on [Fly.io](https://fly.io) with PostgreSQL.

## Prerequisites

- [Fly CLI](https://fly.io/docs/flyctl/install/) installed and authenticated (`fly auth login`)
- A Fly account with billing configured

## Quick Start

### 1. Launch the app

From the repository root:

```bash
fly launch --config deploy/fly/fly.toml --no-deploy
```

Fly will prompt you to set the app name and region. Choose a region close to your team.

### 2. Create a PostgreSQL database

```bash
fly postgres create --name zopp-db
```

Choose the same region as your app. For development, the "Development" plan is sufficient.

### 3. Attach the database

```bash
fly postgres attach zopp-db --app <your-app-name>
```

This automatically sets the `DATABASE_URL` secret on your app.

### 4. Deploy

```bash
fly deploy --config deploy/fly/fly.toml
```

The first deploy takes a few minutes to build the Docker image.

### 5. Verify the deployment

```bash
fly status --app <your-app-name>
```

Check that the Machine is running and healthy.

## Generate an Invite Token

After deploying, create the first invite token to bootstrap your workspace:

```bash
fly ssh console --app <your-app-name> -C "/usr/local/bin/zopp-server invite create"
```

This outputs an invite token. Save it securely — you'll use it to join the server from the CLI.

## Connect the CLI

With the invite token from the previous step:

```bash
zopp --server https://<your-app-name>.fly.dev join <invite-token> <your-email>
```

This registers your device as a principal on the server. You can then create workspaces and manage secrets:

```bash
zopp workspace create my-workspace
zopp project create my-project
zopp environment create production
zopp secret set API_KEY "my-secret-value"
```

## Environment Variables

### Set automatically

| Variable | Description |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string (set by `fly postgres attach`) |

### Configured in fly.toml

| Variable | Default | Description |
|---|---|---|
| `ZOPP_EMAIL_VERIFICATION_REQUIRED` | `false` | Set to `true` once email is configured |

### Optional (set as Fly secrets)

Set these with `fly secrets set`:

```bash
# Enable email verification with Resend
fly secrets set \
ZOPP_EMAIL_VERIFICATION_REQUIRED=true \
ZOPP_EMAIL_PROVIDER=resend \
ZOPP_EMAIL_FROM=noreply@yourdomain.com \
RESEND_API_KEY=re_xxxxx

# Or use SMTP
fly secrets set \
ZOPP_EMAIL_VERIFICATION_REQUIRED=true \
ZOPP_EMAIL_PROVIDER=smtp \
ZOPP_EMAIL_FROM=noreply@yourdomain.com \
SMTP_HOST=smtp.example.com \
SMTP_PORT=587 \
SMTP_USERNAME=user \
SMTP_PASSWORD=pass
```

## TLS

Fly automatically terminates TLS at the edge. The zopp-server runs plain gRPC internally — no TLS certificate configuration is needed.

Your CLI connects via `https://<your-app-name>.fly.dev` and Fly handles the TLS termination.

## Scaling

The default configuration uses a shared CPU with 256MB RAM and auto-stop enabled. To adjust:

```bash
# Scale VM size
fly scale vm shared-cpu-2x --memory 512 --app <your-app-name>

# Keep at least one Machine running (disable auto-stop)
fly scale count 1 --app <your-app-name>
```

## Troubleshooting

### Check server logs

```bash
fly logs --app <your-app-name>
```

### Check health

```bash
fly checks list --app <your-app-name>
```

### SSH into the Machine

```bash
fly ssh console --app <your-app-name>
```
Loading
Loading