Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ede6a21
Add organization schema + enable Better-Auth organization plugin
czxtm Apr 24, 2026
9368b38
Add paidProcedure middleware + user_subscription mirror
czxtm Apr 24, 2026
57fea16
Add hosted alchemy state: schema + envelope encryption + tRPC router
czxtm Apr 24, 2026
a360181
Handle Polar subscription webhooks → user_subscription mirror
czxtm Apr 24, 2026
4ad31a8
Scaffold apps/api for Fly — Hono + tRPC + Better-Auth wrapper
czxtm Apr 24, 2026
74146d9
Make alchemyState put/delete concurrency-check optional; add listStacks
czxtm Apr 24, 2026
de6fa22
Add HostedStateStore adapter for alchemy-effect
czxtm Apr 24, 2026
01ce78f
Add stackpanel.deploy.stateBackend Nix option
czxtm Apr 24, 2026
8cd49e7
Polar product IDs: env-keyed map (dev / preview / production)
czxtm Apr 24, 2026
b5f7346
Register Fly-deploy secrets in shared SOPS
czxtm Apr 24, 2026
75f7526
Switch apps/api to Nix-native container + Fly deploy pipeline
czxtm Apr 24, 2026
84289fc
Add deploy-api GitHub Actions workflow
czxtm Apr 24, 2026
b0cdc2a
deploy-api: allow triggering from feat/cloud-gate-* branches for pre-…
czxtm Apr 24, 2026
b92214a
push-secrets: read from CI-accessible deploy sops payload
czxtm Apr 24, 2026
cb976b4
Simplify push-secrets.sh to use `sops --output-type dotenv`
czxtm Apr 24, 2026
e60a6ea
Rotate github_actions AGE recipient + rekey sops files
czxtm Apr 24, 2026
684677c
deploy-api: trigger on gen/env + .sops.yaml + .stack/config.nix changes
czxtm Apr 24, 2026
6adcc2b
push-secrets: use flyctl (the action installs as `flyctl`, not `fly`)
czxtm Apr 24, 2026
8392346
Switch container backend to dockerTools
czxtm Apr 24, 2026
fb8a2a4
apps/api: build to .output/server/index.mjs for container deploy
czxtm Apr 24, 2026
753dd9c
ci: retrigger deploy-api after FLY_API_TOKEN rotation
czxtm Apr 24, 2026
20ef190
ci: trigger deploy-api after FLY_API_TOKEN rotation
czxtm Apr 24, 2026
a136a2b
ci: bump deploy-api trigger (post-token rotation)
czxtm Apr 24, 2026
f758ec7
deploy-api: run `bun run build` before container image (materializes …
czxtm Apr 24, 2026
28962f7
deploy-api: --rebuild the container so fresh .output is actually used
czxtm Apr 24, 2026
e876049
Move apps/api off Fly to Cloudflare Workers via alchemy-effect
czxtm Apr 24, 2026
f5c4aff
apps/api: sync lockfile after @stackpanel/api-server rename
czxtm Apr 24, 2026
39e17f5
deploy-api: inject ALCHEMY_STATE_TOKEN placeholder + drop CF envs
czxtm Apr 24, 2026
3869fc8
Add common.sops.yaml with alchemy-state-token placeholder
czxtm Apr 24, 2026
95caeea
WIP: apps/api Workers deploy still hitting bundler issue
czxtm Apr 24, 2026
f4b5380
Revert apps/api to Fly, fix mkAppDir to read from STACKPANEL_ROOT_ABS…
czxtm Apr 24, 2026
ddc7355
deploy-api: add ls+print-build-logs diagnostics to container build step
czxtm Apr 24, 2026
d5b88f4
mkAppDir: trace path resolution (temporary diagnostic)
czxtm Apr 24, 2026
28063fe
deploy-api: hoist STACKPANEL_ROOT_ABSOLUTE to job env; drop trace
czxtm Apr 24, 2026
2cf7aed
apps/api: declarative cert+DNS via @distilled.cloud/fly-io
czxtm Apr 24, 2026
a4ffd76
Rotate cloudflare_api_token (now has Zone.DNS Edit) + drop wrangler.toml
czxtm Apr 24, 2026
8a181f8
apps/web: production hostname → local.stackpanel.com (studio is local…
czxtm Apr 24, 2026
9211405
deploy-{web,docs}: drop stale prod-vs-dev SOPS_AGE_KEY split
czxtm Apr 24, 2026
51b5337
infra: migrate Resource provider API for alchemy-effect 0.12
czxtm Apr 25, 2026
af39f31
deploy-docs: pass --yes to skip interactive Plan UI
czxtm Apr 25, 2026
67b87e5
docs: pass full assets config to Cloudflare.Worker
czxtm Apr 25, 2026
3792e8b
docs: pass isExternal:true to Cloudflare.Worker
czxtm Apr 25, 2026
e291338
beads: file stackpanel-dh5 (docs prod deploy 1101)
czxtm Apr 25, 2026
f181369
docs: bump worker compatibility_date to 2026-03-17 for node:perf_hooks
czxtm Apr 25, 2026
599833a
beads: stackpanel-dh5 — perf_hooks fixed; shiki external still 500s
czxtm Apr 25, 2026
ce4249b
web: bind apex stackpanel.com alongside local.stackpanel.com
czxtm Apr 25, 2026
c3b1a4d
feat: production stacks landing, beta waitlist, demo studio
czxtm Apr 29, 2026
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
2 changes: 1 addition & 1 deletion .beads/export-state.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"last_dolt_commit":"gti423bd3bhcledmn7g16da4sho428d8","timestamp":"2026-04-23T20:46:24.660646-07:00","issues":51,"memories":0}
{"last_dolt_commit":"seovg8a6i7ompl8m58c6ckvt5grfi0rl","timestamp":"2026-04-24T21:08:54.011323-07:00","issues":52,"memories":0}
35 changes: 18 additions & 17 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions .github/workflows/deploy-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: deploy-api

# Deploys apps/api to Fly.
#
# Linux runners because skopeo-nix2container doesn't build on darwin (upstream
# bug in the vendored go.podman.io path). The whole container pipeline — build,
# push, deploy — runs on ubuntu-latest where the standard nix2container
# toolchain works without patches.
#
# Triggers:
# - push to main touching apps/api/** or packages/api/**
# - manual via workflow_dispatch

on:
push:
# Include feature branches matching feat/cloud-gate-* so we can verify
# the deploy pipeline before merging to main. Production deploys still
# gate on main.
branches: [main, "feat/cloud-gate-**"]
paths:
- "apps/api/**"
- "packages/api/**"
- "packages/auth/**"
- "packages/db/**"
- "packages/gen/env/**"
- ".sops.yaml"
- ".stack/config.nix"
- ".stack/config.apps.nix"
- "nix/**"
- ".github/workflows/deploy-api.yaml"
workflow_dispatch:
inputs:
skip_build:
description: "Skip container build/push (deploy last-pushed image)"
type: boolean
default: false

concurrency:
group: deploy-api-${{ github.ref }}
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
# mkAppDir reads .output via this absolute path so freshly-built
# untracked output (apps/api/.output/server/index.mjs) lands in the
# nix store. Must be visible to BOTH `nix build` and the push step
# (each re-evaluates the flake separately) — set at job scope so
# the derivation hash matches across steps.
STACKPANEL_ROOT_ABSOLUTE: ${{ github.workspace }}
steps:
- uses: actions/checkout@v4

- uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: |
accept-flake-config = true

- uses: DeterminateSystems/magic-nix-cache-action@main

- uses: superfly/flyctl-actions/setup-flyctl@master

- name: Install sops
run: |
curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64
chmod +x sops-v3.11.0.linux.amd64
sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops

- name: Stage Fly secrets from SOPS
run: bash apps/api/scripts/push-secrets.sh

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11

- name: Build api bundle (produces apps/api/.output for the container)
if: inputs.skip_build != true
run: |
bun install --frozen-lockfile
cd apps/api && bun run build

- name: Build container image
if: inputs.skip_build != true
run: nix build --impure .#packages.x86_64-linux.container-api

- name: Push container image to Fly registry
if: inputs.skip_build != true
run: |
nix run --impure .#copy-container-api -- \
docker://registry.fly.io/ \
--dest-creds "x:${FLY_API_TOKEN}"

- name: Deploy
run: |
flyctl deploy \
--config apps/api/fly.toml \
--app stackpanel-api \
--image registry.fly.io/stackpanel-api:latest \
--wait-timeout 300

- name: Verify health
run: |
curl -fsS --retry 5 --retry-delay 5 \
https://stackpanel-api.fly.dev/health

# Idempotent ACME cert + Cloudflare A/AAAA records pointing at the
# Fly app's IPs. Skipped on dev (the .fly.dev hostname is enough)
# and only kicks in for production / staging / pr-N stages — see
# apps/api/alchemy.run.ts.
- name: Bind public hostname
if: github.ref_name == 'main' || github.ref_name == 'develop' || github.event_name == 'pull_request'
env:
FLY_IO_API_KEY: ${{ secrets.FLY_API_TOKEN }}
STAGE: ${{ github.ref_name == 'main' && 'production' || github.ref_name == 'develop' && 'staging' || format('pr-{0}', github.event.pull_request.number) }}
working-directory: apps/api
run: bunx alchemy-effect deploy --stage "$STAGE"

10 changes: 7 additions & 3 deletions .github/workflows/deploy-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,14 @@ jobs:
# SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the
# generated per-app payloads. `production` uses the prod key;
# everything else (staging, pr-*, dev) uses the dev key.
SOPS_AGE_KEY: ${{ needs.stage.outputs.stage == 'production' && secrets.SECRETS_AGE_KEY_PROD || secrets.SECRETS_AGE_KEY_DEV }}
# All stages encrypt with the github_actions age recipient
# (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split
# was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod
# payloads, so production deploys failed at decrypt time.
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
run: |
set -euo pipefail
bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }}
bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }} --yes
- name: Comment preview URL on PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
Expand Down Expand Up @@ -139,7 +143,7 @@ jobs:
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
run: |
set -euo pipefail
bunx alchemy-effect destroy --stage ${{ needs.stage.outputs.stage }}
bunx alchemy-effect destroy --stage ${{ needs.stage.outputs.stage }} --yes
- name: Delete cached alchemy state
if: always()
env:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/deploy-web.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ jobs:
# SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the
# generated per-app payloads. `production` uses the prod key;
# everything else (staging, pr-*, dev) uses the dev key.
SOPS_AGE_KEY: ${{ needs.stage.outputs.stage == 'production' && secrets.SECRETS_AGE_KEY_PROD || secrets.SECRETS_AGE_KEY_DEV }}
# All stages encrypt with the github_actions age recipient
# (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split
# was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod
# payloads, so production deploys failed at decrypt time.
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
run: |
set -euo pipefail
bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }} --yes
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
.artifacts
release.tar.gz
.stackpanel-root
# `.stackpanel-root` holds a single "." so the stackpanel-root flake
# input resolves to the flake source dir across machines. Must be
# tracked so the flake input can read it during pure evaluation.
*.local.json
.worktrees/
.patch-work/
# Dependencies
node_modules
.pnp
Expand Down
2 changes: 1 addition & 1 deletion .sops.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ keys:
- &coopmoney_3 age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p
- &coopmoney_4 age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt
- &fkb032 age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3
- &github_actions age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf
- &github_actions age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj
- &jjkoh95 age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t
- &keyservice age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp
- &local age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f
Expand Down
37 changes: 37 additions & 0 deletions .stack/config.apps.nix
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,43 @@ let
};
in
{
# apps/api
api = {
name = "api";
description = "Cloud API (Better-Auth, Polar webhooks, hosted alchemy state).";
path = "apps/api";
type = "bun";

bun.generateFiles = false;

env = {
PORT = {
value = "3000";
};
}
// envs.shared;

container = {
enable = true;
type = "bun";
port = 3000;
};

deployment = {
enable = true;
host = "fly";
fly = {
appName = "stackpanel-api";
region = "iad";
memory = "512mb";
cpus = 1;
autoStop = "stop";
minMachines = 1;
forceHttps = true;
};
};
};

# apps/docs
docs = {
name = "docs";
Expand Down
40 changes: 37 additions & 3 deletions .stack/config.nix
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@
warnIfMissing = true;
};
settings = {
backend = "nix2container";
# dockerTools emits a standard docker-archive tarball that the
# system skopeo can push without patches. nix2container's
# skopeo-nix2container currently fails to build against skopeo 1.20
# (upstream patches `vendor/go.podman.io/image/v5` but that path no
# longer exists in the vendored tree). Switch back once the
# upstream bug is fixed.
backend = "dockerTools";
};
};

Expand Down Expand Up @@ -304,6 +310,31 @@
secret = true;
sops = "/dev/postgres-url";
};

# Fly api deploy secrets — routed through the CI-accessible deploy
# scope so the deploy workflow can decrypt them. push-secrets.sh
# reads from the rendered deploy payload, not shared.sops.yaml
# directly (which is encrypted only for human users' AGE keys).
BETTER_AUTH_SECRET = {
secret = true;
sops = "/shared/better-auth-secret";
};
POLAR_ACCESS_TOKEN = {
secret = true;
sops = "/shared/polar-access-token";
};
POLAR_WEBHOOK_SECRET = {
secret = true;
sops = "/shared/polar-webhook-secret";
};
POLAR_PRO_PRODUCT_ID_PRODUCTION = {
secret = true;
sops = "/shared/polar-pro-product-id-production";
};
POLAR_FREE_PRODUCT_ID_PRODUCTION = {
secret = true;
sops = "/shared/polar-free-product-id-production";
};
};
};

Expand Down Expand Up @@ -749,10 +780,13 @@
};
local = {
public-key = "age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f";
tags = [ "dev" ];
tags = [
"dev"
"deploy"
];
};
github-actions = {
public-key = "age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf";
public-key = "age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj";
tags = [
"dev"
"staging"
Expand Down
Loading
Loading