- Runtime: Bun
- Package Manager: Bun
- Development Environment: Devbox
nix --version
direnv --version
devbox versionFile uploads go directly from the browser to Cloudflare R2 using signed URLs. The app server only issues signed upload instructions — it never proxies large file bodies. The worker downloads files from object storage for processing, then deletes the raw file after success.
Configure the following env vars:
S3_ENDPOINT— Cloudflare R2 endpoint (e.g.https://<ACCOUNT_ID>.r2.cloudflarestorage.com)S3_ACCESS_KEY_ID— R2 access keyS3_SECRET_ACCESS_KEY— R2 secret keyS3_BUCKET— bucket nameS3_REGION— region (useautofor R2)
The bucket must have CORS configured to allow PUT requests from the web app origin for direct browser uploads.
Uploaded files are temporary and deleted after successful processing.
- Open the terminal, install the devbox if not yet installed:
curl -fsSL https://get.jetify.com/devbox | bash- Same terminal or new terminal and go to the project root
cd <project-root>
## install devbox
devbox install
## close the terminal and open a new one
## if direnv is not yet allowed run
direnv allow
## run the env-decrypt script
env-decrypt
# Rename all instances of `imeetly` to your project name
# then proceed to Getting Started
## run the init-local script
init-local- Run the migration:
## open the other terminal
## if you have new changes in the `packages/db/src/schemas/*.ts` file
## or no generated folder yet in the `packages/db/src/generated`
## run the generate and migration
bun db:generate
# then
bun db:migrate
## else, just run the migration
bun db:migrate- Open the terminal, install the devbox if not yet installed:
curl -fsSL https://get.jetify.com/devbox | bash- Same terminal or new terminal and go to the project root
cd <project-root>
## install devbox
devbox install
## close the terminal and open a new one
## if direnv is not yet allowed run
direnv allow
## run the env-decrypt script
env-decrypt
# Rename all instances of `imeetly` to your project name
# then proceed to Getting Started- Install dependencies:
bun install
# or
bun i- Generate cert file
cert-gen- Sync env
## run env-decrypt script
## it will decrypt and create a file in `tools/secrets/apps.local.yaml`
env-decrypt
## then run env-sync script
## it will create .env file to the designated directory
env-sync
## if you have new changes in the `tools/secrets/apps.local.yaml` file
## run the env-encrypt script
env-encrypt
## then push the changes to the repo
## so that the encrypted file can be used by other developers
- Sync hosts
sync-hosts- Run the development services:
dev up- Run the migration:
## op the other terminal
## if you have new changes in the `packages/db/src/schemas/*.ts` file
## or no generated folder yet in the `packages/db/src/generated`
## run the generate and migration
bun db:generate
# then
bun db:migrate
## else, just run the migration
bun db:migrate## Open terminal and go to the project root
## run the following commands
reset-local
## on the other terminal
## if you have new changes in the `packages/db/src/schemas/*.ts` file
## or if no generated folder yet in the `packages/db/src/generated`
## run the generate and migration
bun db:generate
# then
bun db:migrate
## else, just run the migration
bun db:migrate## Open terminal and go to the project root
## run the command
reset-db## more info of running services is in `process-compose.yaml`
dev logs <service-name>
## example
dev logs serverTo access the web app, navigate to https://app.imeetly.dev with your browser.
cd <package-path-folder>
bun add <package-name>
## example
bun add pg
## or use the custom script
bunadd pg <package-path-folder>
## example
bunadd pg apps/servercd <package-path-folder>
bun add -D <package-name>
## example
bun add -D pg
## or use the custom script
bundevadd pg <package-path-folder>
## example
bundevadd pg apps/servercd <package-path-folder>
bun rm <package-name>
## example
cd apps/server
bun rm pg
## or use the custom script
bunrm pg <package-path-folder>
## example
bunrm pg apps/serverbun outdated -rbun update --interactive --recursive
# or
bun update -i -rcd <project-root>
uiadd <component-name> <package-path-folder>
## example
uiadd button apps/web| Service | Runtime | Port | Public | Dockerfile |
|---|---|---|---|---|
| web | Bun (SSR) | 3000 | Yes | apps/web/Dockerfile |
| server | Bun (compiled binary) | 4000 | Yes | apps/server/Dockerfile |
| worker | Node.js 22 | — | No | apps/worker/Dockerfile |
External services: PostgreSQL, Temporal, Cloudflare R2.
- VPS: Hetzner (2 vCPU / 4 GB RAM starting)
- Deployment: Dokploy (manages containers on VPS)
- CI/CD: GitHub Actions → Dokploy webhook
- Object Storage: Cloudflare R2
- Database: PostgreSQL (managed or self-hosted)
- Workflow Engine: Temporal (self-hosted or Temporal Cloud)
Upload flow: browser → signed PUT URL → Cloudflare R2 → worker downloads → processes → deletes raw file. The server never proxies large media uploads.
| Service | Build | Start |
|---|---|---|
| web | vite build |
bun run dist/server/server.js |
| server | bun build --compile ... |
./server (standalone binary) |
| worker | bunx tsc --project tsconfig.json |
dotenv -e ../../.env -- tsx src/worker.ts |
- Install Dokploy on your VPS
- Create a Compose project in Dokploy pointing to this repo
- Set compose file path to
docker-compose.prod.yml - Copy all env vars from
.env.exampleinto Dokploy environment settings - Set up domains + SSL for web and server services via Dokploy
- Create a Dokploy webhook, copy the URL
- Add
DOKPLOY_WEBHOOK_URLas a GitHub Actions secret - Push to
main— CI runs, then deploy triggers automatically
| Service | Visibility | Domain |
|---|---|---|
| web | Public (reverse proxy) | app.yourdomain.com |
| server | Public (reverse proxy) | api.yourdomain.com |
| worker | Internal only | No domain needed |
| Workflow | Trigger | Steps |
|---|---|---|
CI (ci.yml) |
push/PR to main |
install → lint → typecheck → test |
Deploy (deploy.yml) |
push to main |
CI → Dokploy webhook |
Required GitHub Secrets:
| Secret | Purpose |
|---|---|
DOKPLOY_WEBHOOK_URL |
Dokploy deployment webhook URL |
See .env.example for the full list. Per-service requirements:
| Variable | web | server | worker |
|---|---|---|---|
APP_ENV, APP_MODULE |
yes | yes | yes |
DATABASE_URL |
yes | yes | yes |
S3_* |
— | yes | yes |
TEMPORAL_* |
— | yes | yes |
CORS_* |
— | yes | — |
GOOGLE_CLIENT_* |
— | yes | — |
POLAR_* |
— | yes | — |
RESEND_API_KEY |
— | yes | — |
OPENAI_API_KEY (optional) |
— | — | yes |
ANTHROPIC_API_KEY (optional) |
— | — | yes |
GROQ_API_KEY (optional) |
— | — | yes |
VITE_* |
yes | — | — |
AI provider keys are optional — missing keys skip that provider.
- web:
GET /health/live - server:
GET /rpc/health/live(liveness),GET /rpc/health/ready(readiness) - worker: Process-level (Temporal SDK connection heartbeat)
packages/dbexports shared schema and types only- web/server use
bun:sql+drizzle-orm/bun-sql(Bun runtime) - worker uses
pg+drizzle-orm/node-postgres(Node.js runtime) - Worker must never import
@repo/db's runtime client — it has its owndb.ts - Worker Dockerfile uses
node:22-slimas final image
Object storage is temporary and internal only:
- No user-facing storage features
- Raw files are deleted after successful processing
- Bucket CORS must be configured for direct browser uploads (
tools/s3/setup-cors.ts) - VPS and object storage are both in Helsinki for low internal latency