Web application that sends SMS messages about upcoming waste collection dates.
Monorepo using pnpm workspace with modular packages shared across apps:
- apps/user-application - TanStack Start consumer-facing app
- apps/data-service - Backend service for long-running tasks
- packages/data-ops - Shared DB layer (schemas, queries, auth)
Stack: Better Auth, Drizzle ORM, Cloudflare Workers, Neon Postgres.
Central shared package for all database operations. Both apps consume this package for type-safe DB access.
Purpose: Single source of truth for database schemas, queries, validations, and auth config.
Core database definitions using Drizzle ORM.
schema.ts- Main application tables (cities, streets, addresses, notification_preferences)auth-schema.ts- Better Auth tables (auto-generated, don't edit manually)relations.ts- Drizzle relational queries config (defines joins between tables)migrations/{env}/- Migration history per environment (dev/stage/prod)
Reusable database operations exported as functions.
Example: user.ts exports getUserProfile(), updateUserPhone()
Usage: Import and call from apps - handles DB connection internally via getDb().
import { getUserProfile } from "data-ops/queries/user";
const user = await getUserProfile(userId);API request/response validation schemas using Zod.
Purpose: Type-safe contracts between frontend/backend. Validates data shape at runtime.
Example: user.ts exports UserProfileResponse schema.
setup.ts- DB client initialization (getDb()function)seed/- Data seeding utilities (file loader, importer)
Better Auth configuration.
setup.ts- Auth config (providers, plugins)server.ts- Auth server instance
- Add table to
src/drizzle/schema.ts - Add relations to
src/drizzle/relations.ts(if needed) - Generate migration:
pnpm run drizzle:dev:generate - Apply migration:
pnpm run drizzle:dev:migrate - Create queries in
src/queries/{feature}.ts - Create Zod schemas in
src/zod-schema/{feature}.ts - Rebuild package:
pnpm run build:data-ops - Import in apps: Use queries/schemas from both user-application and data-service
pnpm run setupInstalls all dependencies and builds data-ops package.
pnpm run dev:user-application # TanStack Start app (port 3000)
pnpm run dev:data-service # Hono backend serviceFrom packages/data-ops/ directory:
pnpm run drizzle:dev:generate # Generate migration
pnpm run drizzle:dev:migrate # Apply to databaseReplace dev with stage or prod. Migrations stored in src/drizzle/migrations/{env}/.
Note: Stage/prod migrations run automatically in deploy workflows. Only run
devmigrations locally.
Config files in packages/data-ops/:
.env.dev- Local development.env.stage- Staging.env.prod- Production
Required:
DATABASE_HOST=
DATABASE_USERNAME=
DATABASE_PASSWORD=Deployments are handled by GitHub Actions — do not deploy manually.
Automatically deploys on every merge to main via .github/workflows/deploy-stage.yml.
Pipeline: install → build data-ops → generate DB migrations → apply migrations → deploy data-service → deploy user-application.
URL: https://stage.powiadomienia.info
Manual trigger only via .github/workflows/deploy-prod.yml (requires reviewer approval in GitHub Actions tab).
Same pipeline as staging, targeting production DB and Cloudflare Workers.
URL: https://powiadomienia.info
Per environment (stage / production):
| Secret | Purpose |
|---|---|
DATABASE_URL |
Full Neon Postgres connection string for migrations (e.g. postgresql://user:pass@host/db?sslmode=require) |
Repository-level:
| Secret | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN |
Workers deploy |
CLOUDFLARE_ACCOUNT_ID |
Workers deploy |
NEON_API_KEY |
CI branch management |
Application secrets (Telegram, SerwerSMS, etc.) are managed via wrangler secret put per environment.
- Cron runs every hour (e.g., 8:00 AM CET, 9:00 AM CET, etc.)
- Scheduled handler queries users where notification_preferences.hour = current_hour
- 2.1 At 8:00 AM CET → only finds users with hour = 8
- 2.2 At 9:00 AM CET → only finds users with hour = 9
- Matched users get queued immediately
- Queue consumer processes and sends SMS within seconds (not hours)
7:59 AM CET - Cron hasn't run yet, nothing happens
8:00 AM CET - Cron runs
└─ 8:00:01 - Query finds users with hour=8
└─ 8:00:02 - Messages queued to NOTIFICATION_QUEUE
└─ 8:00:03 - Queue consumer starts processing
└─ 8:00:04 - SMS sent via SerwerSMS
└─ 8:00:05 - Notification logged
9:00 AM CET - Cron runs again (different users with hour=9)