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}/.
Config files in packages/data-ops/:
.env.dev- Local development.env.stage- Staging.env.prod- Production
Required:
DATABASE_HOST=
DATABASE_USERNAME=
DATABASE_PASSWORD=Once the deployment is done, Cloudflare will response with URL to view the deployment. If you want to change the name associated with Worker, do so by changing the name in the wrangler.jsonc file.
You can also use your own domain names associated with Cloudflare account by adding a route to this file as well.
pnpm run deploy:stage:user-applicationThis will deploy the user-application to Cloudflare Workers into staging environment.
pnpm run deploy:prod:user-applicationThis will deploy the user-application to Cloudflare Workers into production environment.
Once the deployment is done, Cloudflare will response with URL to view the deployment. If you want to change the name associated with Worker, do so by changing the name in the wrangler.jsonc file.
You can also use your own domain names associated with Cloudflare account by adding a route to this file as well.
pnpm run deploy:stage:data-servicepnpm run deploy:prod:data-service- 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)