Skip to content

Commit b5be4c5

Browse files
authored
feat: v2.5.0 T2 — AWS Lambda deployment (entry + SAM + workflow + docs) (#331)
- server/entry-lambda.ts: Hono app via hono/aws-lambda handle(); lazy init pattern for CJS compatibility and warm-start reuse; serves SPA static files from dist/ with MIME detection and index.html fallback - deploy/aws-lambda/template.yaml: SAM template with Function URL (no API Gateway), Node 22, TURSO_* / BETTER_AUTH_SECRET / APP_URL env vars, minimal IAM (AWSLambdaBasicExecutionRole) - .github/workflows/deploy-aws-lambda.yml: 8-step contract (guard upstream, check secrets with exact names, resolve tag, checkout, ensure SAM artifact bucket, apply Turso migrations, build + sam deploy, post-deploy auto-gen BETTER_AUTH_SECRET + patch BETTER_AUTH_URL + write URL to summary) - package.json: build:lambda script (tsup CJS, external @libsql/client) - docs/deploy/aws-lambda.md: Prerequisites / Secrets / Trigger / First-boot storage / Cost sections - README.md, V2_ROADMAP.md: link new doc Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
1 parent 5593eec commit b5be4c5

8 files changed

Lines changed: 427 additions & 1 deletion

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
name: Deploy to AWS Lambda
2+
3+
on:
4+
push:
5+
branches: [master]
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: 'Release tag to deploy (e.g. v2.5.0). Leave empty for latest.'
10+
required: false
11+
12+
# Prevent overlapping deployments.
13+
concurrency:
14+
group: deploy-aws-lambda
15+
cancel-in-progress: false
16+
17+
jobs:
18+
deploy:
19+
name: Deploy
20+
runs-on: ubuntu-latest
21+
# Only run on forks — the upstream repo does not deploy itself.
22+
if: github.repository != 'saltbo/zpan'
23+
24+
steps:
25+
- name: Disable upstream-only workflows
26+
env:
27+
GH_TOKEN: ${{ github.token }}
28+
run: |
29+
for workflow in ci.yml release.yml; do
30+
gh api -X PUT "repos/${{ github.repository }}/actions/workflows/$workflow/disable" 2>/dev/null && \
31+
echo "Disabled $workflow" || echo "$workflow already disabled"
32+
done
33+
34+
- name: Check required secrets
35+
env:
36+
HAS_TURSO_URL: ${{ secrets.TURSO_DATABASE_URL != '' }}
37+
HAS_TURSO_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN != '' }}
38+
HAS_AWS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID != '' }}
39+
HAS_AWS_SECRET: ${{ secrets.AWS_SECRET_ACCESS_KEY != '' }}
40+
HAS_AWS_REGION: ${{ secrets.AWS_REGION != '' }}
41+
run: |
42+
missing=""
43+
[ "$HAS_TURSO_URL" != "true" ] && missing="$missing TURSO_DATABASE_URL"
44+
[ "$HAS_TURSO_TOKEN" != "true" ] && missing="$missing TURSO_AUTH_TOKEN"
45+
[ "$HAS_AWS_KEY" != "true" ] && missing="$missing AWS_ACCESS_KEY_ID"
46+
[ "$HAS_AWS_SECRET" != "true" ] && missing="$missing AWS_SECRET_ACCESS_KEY"
47+
[ "$HAS_AWS_REGION" != "true" ] && missing="$missing AWS_REGION"
48+
if [ -n "$missing" ]; then
49+
echo "::error::Missing required secrets:$missing. Go to Settings → Secrets and variables → Actions and add them."
50+
exit 1
51+
fi
52+
53+
- name: Resolve release tag
54+
id: release
55+
env:
56+
GH_TOKEN: ${{ github.token }}
57+
INPUT_VERSION: ${{ inputs.version }}
58+
run: |
59+
if [ -n "$INPUT_VERSION" ]; then
60+
TAG="$INPUT_VERSION"
61+
else
62+
TAG=$(gh api repos/saltbo/zpan/releases/latest --jq '.tag_name')
63+
fi
64+
if [ -z "$TAG" ]; then
65+
echo "::error::No release found in saltbo/zpan"
66+
exit 1
67+
fi
68+
echo "version=$TAG" >> "$GITHUB_OUTPUT"
69+
echo "### 🚀 Deploying $TAG to AWS Lambda" >> "$GITHUB_STEP_SUMMARY"
70+
71+
- uses: actions/checkout@v4
72+
with:
73+
repository: saltbo/zpan
74+
ref: ${{ steps.release.outputs.version }}
75+
76+
- uses: actions/setup-node@v4
77+
with:
78+
node-version: 22
79+
80+
- name: Configure AWS credentials
81+
uses: aws-actions/configure-aws-credentials@v4
82+
with:
83+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
84+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
85+
aws-region: ${{ secrets.AWS_REGION }}
86+
87+
- name: Setup SAM CLI
88+
uses: aws-actions/setup-sam@v2
89+
90+
- run: npm ci
91+
92+
- name: Ensure SAM artifact bucket exists
93+
id: bucket
94+
run: |
95+
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
96+
BUCKET="zpan-sam-artifacts-${ACCOUNT}-${{ secrets.AWS_REGION }}"
97+
if ! aws s3api head-bucket --bucket "$BUCKET" 2>/dev/null; then
98+
aws s3 mb "s3://$BUCKET" --region "${{ secrets.AWS_REGION }}"
99+
echo "Created SAM artifact bucket: $BUCKET"
100+
else
101+
echo "Reusing SAM artifact bucket: $BUCKET"
102+
fi
103+
echo "name=$BUCKET" >> "$GITHUB_OUTPUT"
104+
105+
- name: Apply Turso migrations
106+
env:
107+
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
108+
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
109+
run: npx drizzle-kit migrate
110+
111+
- name: Resolve existing deployment state
112+
id: state
113+
env:
114+
USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
115+
run: |
116+
# Read current function config if it exists (redeployment case)
117+
FUNC_JSON=$(aws lambda get-function-configuration --function-name zpan 2>/dev/null || echo "")
118+
119+
if [ -n "$FUNC_JSON" ]; then
120+
EXISTING_SECRET=$(echo "$FUNC_JSON" | jq -r '.Environment.Variables.BETTER_AUTH_SECRET // empty')
121+
EXISTING_URL=$(aws lambda get-function-url-config --function-name zpan \
122+
--query FunctionUrl --output text 2>/dev/null || echo "")
123+
fi
124+
125+
# Determine BETTER_AUTH_SECRET (priority: user-supplied > existing > auto-generate)
126+
if [ -n "$USER_SECRET" ]; then
127+
SECRET="$USER_SECRET"
128+
echo "Using BETTER_AUTH_SECRET from GitHub secret"
129+
elif [ -n "$EXISTING_SECRET" ]; then
130+
SECRET="$EXISTING_SECRET"
131+
echo "Reusing existing BETTER_AUTH_SECRET"
132+
else
133+
SECRET=$(openssl rand -base64 32)
134+
echo "Auto-generated BETTER_AUTH_SECRET"
135+
fi
136+
137+
echo "secret=$SECRET" >> "$GITHUB_OUTPUT"
138+
echo "app_url=${EXISTING_URL:-}" >> "$GITHUB_OUTPUT"
139+
140+
- name: Build
141+
run: |
142+
npx vite build --mode node
143+
144+
# Bundle Lambda entry; @libsql/client is external (native binding)
145+
npx tsup server/entry-lambda.ts --format cjs --outDir dist-lambda --external @libsql/client
146+
147+
# Assemble minimal Lambda deployment package
148+
mkdir -p dist-lambda-pkg
149+
cp dist-lambda/entry-lambda.cjs dist-lambda-pkg/
150+
cp -r dist dist-lambda-pkg/
151+
cp -r migrations dist-lambda-pkg/
152+
153+
# Minimal package.json so SAM installs only @libsql/client
154+
node -e "
155+
const p = require('./package.json');
156+
const pkg = {
157+
name: 'zpan-lambda',
158+
version: p.version,
159+
dependencies: { '@libsql/client': p.dependencies['@libsql/client'] }
160+
};
161+
require('fs').writeFileSync('dist-lambda-pkg/package.json', JSON.stringify(pkg, null, 2));
162+
"
163+
164+
- name: SAM build
165+
run: sam build --template-file deploy/aws-lambda/template.yaml
166+
167+
- name: SAM deploy
168+
run: |
169+
sam deploy \
170+
--stack-name zpan \
171+
--s3-bucket "${{ steps.bucket.outputs.name }}" \
172+
--capabilities CAPABILITY_IAM \
173+
--no-confirm-changeset \
174+
--no-fail-on-empty-changeset \
175+
--parameter-overrides \
176+
TursoDatabaseUrl="${{ secrets.TURSO_DATABASE_URL }}" \
177+
TursoAuthToken="${{ secrets.TURSO_AUTH_TOKEN }}" \
178+
BetterAuthSecret="${{ steps.state.outputs.secret }}" \
179+
AppUrl="${{ steps.state.outputs.app_url }}"
180+
181+
- name: Finalize — set BETTER_AUTH_URL and write summary
182+
run: |
183+
# Get the actual Function URL from CloudFormation outputs
184+
URL=$(aws cloudformation describe-stacks \
185+
--stack-name zpan \
186+
--query "Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue" \
187+
--output text)
188+
189+
# On first deploy AppUrl was empty; patch the live function so auth works immediately.
190+
CURRENT_URL="${{ steps.state.outputs.app_url }}"
191+
if [ "$URL" != "$CURRENT_URL" ]; then
192+
aws lambda update-function-configuration \
193+
--function-name zpan \
194+
--environment "Variables={
195+
NODE_ENV=production,
196+
TURSO_DATABASE_URL=${{ secrets.TURSO_DATABASE_URL }},
197+
TURSO_AUTH_TOKEN=${{ secrets.TURSO_AUTH_TOKEN }},
198+
BETTER_AUTH_SECRET=${{ steps.state.outputs.secret }},
199+
APP_URL=$URL,
200+
BETTER_AUTH_URL=$URL
201+
}" > /dev/null
202+
echo "Updated BETTER_AUTH_URL → $URL"
203+
fi
204+
205+
echo "" >> "$GITHUB_STEP_SUMMARY"
206+
echo "**URL:** $URL" >> "$GITHUB_STEP_SUMMARY"
207+
echo "" >> "$GITHUB_STEP_SUMMARY"
208+
echo "**Next step:** Open the URL, register the first admin account, then go to Admin → Storages to configure your S3-compatible bucket." >> "$GITHUB_STEP_SUMMARY"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ node_modules/
55
dist/
66
dist-server/
77
api/
8+
dist-lambda/
9+
dist-lambda-pkg/
10+
.aws-sam/
811
.wrangler/
912

1013
# Environment

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ Deploy via GitHub Actions with zero server management. Free tier covers personal
3535

3636
After initial setup, the workflow runs automatically every time you sync your fork with the latest release.
3737

38+
### AWS Lambda
39+
40+
Deploy via GitHub Actions using SAM. Lambda Function URL provides HTTPS with no API Gateway needed.
41+
42+
1. **Fork** this repository
43+
2. In your fork, go to **Settings → Secrets and variables → Actions** and add:
44+
- `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` — from [Turso](https://turso.tech) (free, no credit card)
45+
- `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
46+
3. Go to the **Actions** tab, select **Deploy to AWS Lambda**, and click **Run workflow**
47+
48+
See [docs/deploy/aws-lambda.md](docs/deploy/aws-lambda.md) for full setup instructions and IAM permissions.
49+
3850
### Docker
3951

4052
**Quick start** — pull the pre-built image and bring your own S3 storage:

V2_ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ v2.0–v2.4 ship on two runtimes. v2.5 expands to seven.
4444

4545
- **Cloudflare Workers** — Zero-ops, free tier covers personal use, one-click deploy (all versions)
4646
- **Docker** — Self-hosted, bring your own S3, full control (all versions)
47-
- **AWS Lambda** — For teams on AWS; SAM template + one-click CloudFormation (v2.5+)
47+
- **AWS Lambda** — For teams on AWS; SAM template + GitHub Actions workflow ([docs](docs/deploy/aws-lambda.md)) (v2.5+)
4848
- **Vercel** — One-click "Deploy to Vercel" via GitHub (v2.5+)
4949
- **Netlify** — One-click "Deploy to Netlify" (v2.5+)
5050
- **Azure Functions** — Bicep template; for Azure-mandated environments (v2.5+)

deploy/aws-lambda/template.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: ZPan — S3-native file hosting platform on AWS Lambda
4+
5+
Globals:
6+
Function:
7+
Timeout: 30
8+
MemorySize: 512
9+
Runtime: nodejs22.x
10+
Architectures:
11+
- x86_64
12+
13+
Parameters:
14+
TursoDatabaseUrl:
15+
Type: String
16+
Description: Turso database URL (e.g. libsql://your-db.turso.io)
17+
NoEcho: true
18+
TursoAuthToken:
19+
Type: String
20+
Description: Turso auth token
21+
NoEcho: true
22+
BetterAuthSecret:
23+
Type: String
24+
Description: Signing secret for auth sessions
25+
NoEcho: true
26+
AppUrl:
27+
Type: String
28+
Description: Public Function URL of the deployed application (set after first deploy)
29+
Default: ''
30+
31+
Resources:
32+
ZPanFunction:
33+
Type: AWS::Serverless::Function
34+
Properties:
35+
FunctionName: zpan
36+
CodeUri: ../../dist-lambda-pkg/
37+
Handler: entry-lambda.handler
38+
Description: ZPan file hosting platform
39+
Policies:
40+
- AWSLambdaBasicExecutionRole
41+
Environment:
42+
Variables:
43+
NODE_ENV: production
44+
TURSO_DATABASE_URL: !Ref TursoDatabaseUrl
45+
TURSO_AUTH_TOKEN: !Ref TursoAuthToken
46+
BETTER_AUTH_SECRET: !Ref BetterAuthSecret
47+
APP_URL: !Ref AppUrl
48+
BETTER_AUTH_URL: !Ref AppUrl
49+
50+
FunctionUrlConfig:
51+
AuthType: NONE
52+
Cors:
53+
AllowOrigins:
54+
- '*'
55+
AllowHeaders:
56+
- '*'
57+
AllowMethods:
58+
- '*'
59+
60+
Outputs:
61+
FunctionUrl:
62+
Description: ZPan public URL
63+
Value: !GetAtt ZPanFunctionUrl.FunctionUrl

docs/deploy/aws-lambda.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# AWS Lambda Deployment
2+
3+
ZPan runs on AWS Lambda via a [Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) — no API Gateway required. A [SAM](https://aws.amazon.com/serverless/sam/) template provisions the function and the GitHub Actions workflow deploys it automatically.
4+
5+
## Prerequisites
6+
7+
- An AWS account with permissions to create Lambda functions, IAM roles, and S3 buckets
8+
- A [Turso](https://turso.tech) database (free tier: 9 GB, no credit card required)
9+
- A fork of this repository
10+
11+
### Create a Turso database (~3 minutes)
12+
13+
```bash
14+
curl -sSfL https://get.tur.so/install.sh | bash
15+
turso auth signup # GitHub OAuth
16+
turso db create zpan
17+
turso db show zpan --url # → TURSO_DATABASE_URL
18+
turso db tokens create zpan # → TURSO_AUTH_TOKEN
19+
```
20+
21+
Alternatively, create the database via the [Turso dashboard](https://app.turso.tech) without installing the CLI.
22+
23+
## Secrets
24+
25+
Add the following in your fork under **Settings → Secrets and variables → Actions**:
26+
27+
| Secret | Description |
28+
|--------|-------------|
29+
| `TURSO_DATABASE_URL` | Turso database URL, e.g. `libsql://your-db.turso.io` |
30+
| `TURSO_AUTH_TOKEN` | Turso auth token (rotate via `turso db tokens create zpan`) |
31+
| `AWS_ACCESS_KEY_ID` | AWS access key ID |
32+
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key |
33+
| `AWS_REGION` | AWS region to deploy to, e.g. `us-east-1` |
34+
35+
> **S3 credentials are not here.** ZPan stores your object storage configuration in the database, configured via the Admin UI after the first deploy. This keeps bucket secrets off GitHub and lets you manage multiple storage backends from one place.
36+
37+
> **BETTER_AUTH_SECRET** — If omitted, the workflow auto-generates a secure random value on first deploy and stores it in the Lambda function configuration. Add this secret only if you want to supply your own value.
38+
39+
### IAM permissions for the deploy user
40+
41+
The AWS credentials need these permissions:
42+
43+
```json
44+
{
45+
"Version": "2012-10-17",
46+
"Statement": [
47+
{ "Effect": "Allow", "Action": ["lambda:*", "iam:*", "s3:*", "cloudformation:*"], "Resource": "*" },
48+
{ "Effect": "Allow", "Action": "sts:GetCallerIdentity", "Resource": "*" }
49+
]
50+
}
51+
```
52+
53+
For production, scope the `Resource` fields to specific ARNs. The workflow creates a single S3 bucket (`zpan-sam-artifacts-<account>-<region>`) for SAM deployment artifacts on first run.
54+
55+
## Trigger
56+
57+
1. Fork this repository
58+
2. Add the secrets above
59+
3. Go to the **Actions** tab → **Deploy to AWS Lambda****Run workflow**
60+
61+
The workflow runs automatically on every push to `master` after initial setup. Re-running is idempotent — it redeploys without recreating existing resources.
62+
63+
## First-boot storage setup
64+
65+
After the workflow reports success:
66+
67+
1. Open the Function URL shown in the job summary
68+
2. Register a user (the first user gets admin role)
69+
3. Go to **Admin → Storages → Add storage** and fill in your S3-compatible bucket details:
70+
- **Endpoint**: your S3 endpoint (e.g. `https://s3.amazonaws.com` for AWS S3, or your R2/Tigris/B2 URL)
71+
- **Bucket**: your bucket name
72+
- **Region**: the bucket's region
73+
- **Access Key / Secret Key**: bucket credentials
74+
75+
> The storage endpoint must be reachable from the **client browser**, since ZPan uploads files directly to S3 via presigned URLs — no server bandwidth is used.
76+
77+
## Cost
78+
79+
With the AWS free tier and Turso free tier, personal ZPan usage costs $0/month:
80+
81+
| Resource | Free tier | Notes |
82+
|----------|-----------|-------|
83+
| AWS Lambda | 1M requests / 400,000 GB-seconds / month | Easily covers personal use |
84+
| Lambda Function URL | Included with Lambda | No extra charge |
85+
| S3 (SAM artifacts) | 5 GB / month | One-time ~10 MB upload per deploy |
86+
| Turso | 9 GB storage, 1B row reads / month | Shared across all deployments |
87+
88+
S3 (or R2/Tigris) for ZPan file storage is billed separately and depends on your usage. ZPan itself does not add server-side bandwidth costs because files transfer directly between client and S3.

0 commit comments

Comments
 (0)