Music Store Management Platform Backend Technical Architecture TypeScript | Fastify | Bun | Drizzle ORM | BullMQ Version 1.0 | Draft # 1. Technology Rationale The platform is TypeScript end-to-end. Every application — desktop, mobile, web portals, and API — shares one language, one type system, and one validation library. This is the primary driver of the technology choice: a solo developer maintaining the full stack gets maximum benefit from type safety flowing across every boundary. Decision Rationale TypeScript everywhere One language across API, desktop, mobile, and web. Shared types and validation. Refactoring catches errors across all applications simultaneously. Fastify over Express Better TypeScript support, faster performance, schema-based validation built in, plugin architecture cleaner than Express middleware. Bun over Node.js Native TypeScript execution without compilation step. Faster startup. Built-in compile-to-binary for self-hosted distribution. Compatible with Node.js ecosystem. Drizzle over Prisma Schema defined in TypeScript — no separate DSL file. Fully typed queries. SQL-transparent — generated queries are readable and predictable. BullMQ over Celery TypeScript-native. Redis-based — same Redis already used for caching. No separate Python worker process needed. Zod for validation Single schema definition runs on both backend (request validation) and frontend (form validation). Eliminates duplicate validation logic. # 2. Monorepo Structure The entire platform lives in a single monorepo managed by Turborepo. All packages share the same TypeScript compiler configuration, lint rules, and test runner. Internal packages are referenced by workspace path — no publishing required. / (monorepo root) turbo.json pipeline config package.json workspace root tsconfig.base.json shared TS config /packages /shared shared types, schemas, business logic /backend Fastify API (Bun runtime) /desktop Electron + React /mobile React Native (iOS) /web-portal React (customer facing) /admin React (internal) /tools /installer PowerShell scripts + Go tray app /migration AIM MSSQL -> Postgres ETL /codegen OpenAPI client generation # 3. Shared Package The shared package is the most important piece of the monorepo. It contains everything that is used by more than one application. No business logic lives in individual apps — it lives here. /packages/shared/src /types account.ts Account, Member, PaymentMethod rental.ts Rental, RentalType, RentalStatus lesson.ts Enrollment, LessonSession, LessonPlan repair.ts RepairTicket, RepairBatch, RepairPart inventory.ts Product, InventoryUnit, RepairPart payment.ts Transaction, PaymentMethod, Subscription accounting.ts JournalEntry, AccountCode license.ts License, LicenseModules index.ts re-exports everything /schemas Zod schemas (frontend + backend validation) account.schema.ts rental.schema.ts lesson.schema.ts repair.schema.ts inventory.schema.ts payment.schema.ts /business-logic rto.ts rent-to-own equity calculations pricing.ts discount engine, min price enforcement billing.ts proration calculations, billing groups license.ts license verification, module checks accounting.ts journal entry generation rules /constants modules.ts MODULE_IDS, feature flags tax.ts tax rate helpers /utils currency.ts formatting, rounding helpers dates.ts billing date utilities ## 3.1 Zod Schema Example — Shared Validation // packages/shared/src/schemas/rental.schema.tsimport { z } from 'zod'export const RentalCreateSchema = z.object({ account_id: z.string().uuid(), member_id: z.string().uuid(), inventory_unit_id: z.string().uuid(), rental_type: z.enum([ 'month_to_month', 'rent_to_own', 'short_term', 'lease_purchase' ]), monthly_rate: z.number().positive().max(9999), deposit_amount: z.number().min(0).optional(), billing_anchor_day: z.number().int().min(1).max(28), billing_group: z.string().max(50).optional(), rto_purchase_price: z.number().positive().optional(), rto_equity_percent: z.number().min(0).max(100).optional(),})export type RentalCreate = z.infer// Used in backend: request body validation// Used in desktop: rental intake form validation// Used in mobile: same form, same rules// One definition, zero drift ## 3.2 Business Logic Example — RTO Calculation // packages/shared/src/business-logic/rto.tsexport function calculateRTOEquity(params: { monthlyRate: number equityPercent: number paymentsReceived: number}): { equityAccumulated: number equityPerPayment: number} { const equityPerPayment = (params.monthlyRate * params.equityPercent) / 100 const equityAccumulated = equityPerPayment * params.paymentsReceived return { equityAccumulated, equityPerPayment }}export function calculateBuyoutAmount(params: { purchasePrice: number equityAccumulated: number}): number { return Math.max(0, params.purchasePrice - params.equityAccumulated )}// Same calculation runs on API, desktop, and portal// Customer sees same number the staff sees # 4. Backend Package The backend is a Fastify application running on the Bun runtime. It handles all API requests, payment provider integrations, background job scheduling, and PDF generation. For self-hosted deployments it compiles to a single standalone binary. ## 4.1 Directory Structure /packages/backend/src main.ts entry point — registers plugins, starts server /plugins auth.ts JWT verification, role extraction license.ts license loading, module guard database.ts Drizzle connection pool redis.ts Redis connection cors.ts error-handler.ts /routes /v1 accounts.ts rentals.ts lessons.ts repairs.ts inventory.ts payments.ts accounting.ts reports.ts admin.ts webhooks.ts payment processor webhooks health.ts /services business logic — thin routes call services account.service.ts rental.service.ts lesson.service.ts repair.service.ts billing.service.ts accounting.service.ts pdf.service.ts notification.service.ts /db schema/ Drizzle table definitions accounts.ts rentals.ts lessons.ts repairs.ts inventory.ts payments.ts accounting.ts migrations/ Drizzle migration files index.ts db client export /payment-providers base.ts PaymentProvider interface stripe.ts Stripe implementation global-payments.ts Global Payments implementation factory.ts returns correct provider per store /jobs BullMQ workers billing.job.ts daily GP billing scheduler reminders.job.ts renewal and payment reminders backup.job.ts self-hosted backup trigger reports.job.ts scheduled report generation scheduler.ts registers all jobs with cron /license verify.ts Ed25519 signature verification guard.ts requireModule() Fastify hook ## 4.2 Route Pattern Routes are thin — they validate input using shared Zod schemas, call a service, and return the result. All business logic lives in services. // routes/v1/rentals.tsimport { FastifyPluginAsync } from 'fastify'import { RentalCreateSchema } from '@platform/shared'import { RentalService } from '../../services/rental.service'const rentals: FastifyPluginAsync = async (fastify) => { fastify.post('/', { preHandler: [ fastify.authenticate, // JWT check fastify.requireModule('MOD-RENTALS'), // license check fastify.requireRole('staff'), // role check ], schema: { body: RentalCreateSchema, // Zod validation } }, async (request, reply) => { const rental = await RentalService.create( fastify.db, request.companyId, request.body, request.user ) return reply.code(201).send(rental) } ) fastify.get('/:id', { preHandler: [fastify.authenticate], }, async (request, reply) => { const rental = await RentalService.getById( fastify.db, request.companyId, request.params.id ) if (!rental) return reply.code(404).send() return rental } )}export default rentals ## 4.3 Drizzle Schema Example // db/schema/rentals.tsimport { pgTable, uuid, numeric, integer, timestamp, boolean, text, pgEnum} from 'drizzle-orm/pg-core'import { accounts } from './accounts'import { inventoryUnits } from './inventory'export const rentalTypeEnum = pgEnum('rental_type', [ 'month_to_month', 'rent_to_own', 'short_term', 'lease_purchase'])export const rentalStatusEnum = pgEnum('rental_status', [ 'active', 'returned', 'cancelled', 'completed'])export const rentals = pgTable('rental', { id: uuid('id').primaryKey().defaultRandom(), company_id: uuid('company_id').notNull(), account_id: uuid('account_id') .notNull() .references(() => accounts.id), inventory_unit_id: uuid('inventory_unit_id') .references(() => inventoryUnits.id), rental_type: rentalTypeEnum('rental_type').notNull(), status: rentalStatusEnum('status') .notNull().default('active'), monthly_rate: numeric('monthly_rate', { precision: 10, scale: 2 }).notNull(), billing_anchor_day: integer('billing_anchor_day').notNull(), billing_group: text('billing_group'), stripe_subscription_id: text('stripe_subscription_id'), rto_purchase_price: numeric('rto_purchase_price', { precision: 10, scale: 2 }), rto_equity_percent: numeric('rto_equity_percent', { precision: 5, scale: 2 }), rto_equity_accumulated: numeric('rto_equity_accumulated', { precision: 10, scale: 2 }) .default('0'), legacy_id: text('legacy_id'), created_at: timestamp('created_at').defaultNow(), updated_at: timestamp('updated_at').defaultNow(),})// Fully typed — TypeScript infers the shape from the schemaexport type Rental = typeof rentals.$inferSelectexport type RentalInsert = typeof rentals.$inferInsert ## 4.4 Payment Provider Interface // payment-providers/base.tsexport interface PaymentProvider { readonly name: string // Customers createCustomer(account: Account): Promise deleteCustomer(processorId: string): Promise // Payment methods createSetupSession(): Promise attachPaymentMethod( customerId: string, token: string ): Promise // One-time charges charge(params: ChargeParams): Promise refund(params: RefundParams): Promise // Subscriptions createSubscription( params: SubscriptionParams ): Promise addSubscriptionItem( subscriptionId: string, item: SubscriptionItem ): Promise removeSubscriptionItem( subscriptionId: string, itemId: string ): Promise cancelSubscription(id: string): Promise pauseSubscription(id: string): Promise resumeSubscription(id: string): Promise changeBillingAnchor( subscriptionId: string, day: number ): Promise // Terminal discoverReaders(): Promise collectPayment( params: TerminalPaymentParams ): Promise // Webhooks parseWebhookEvent( payload: string, signature: string ): Promise}// Factory returns correct provider per storeexport function getPaymentProvider( store: Store): PaymentProvider { switch (store.payment_processor) { case 'stripe': return new StripeProvider(store) case 'global_payments': return new GlobalPaymentsProvider(store) default: throw new Error('Unknown processor') }} ## 4.5 BullMQ Billing Scheduler // jobs/billing.job.tsimport { Worker, Queue } from 'bullmq'import { getPaymentProvider } from '../payment-providers/factory'export const billingQueue = new Queue('billing', { connection: redis, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } }})export const billingWorker = new Worker( 'billing', async (job) => { const { companyId, entityType, entityId } = job.data const store = await StoreService.getById(db, companyId) const provider = getPaymentProvider(store) if (entityType === 'rental') { const rental = await RentalService.getById( db, companyId, entityId ) await provider.charge({ customerId: rental.account.processor_customer_id, amount: rental.monthly_rate, description: `Rental - ${rental.product_name}`, metadata: { rental_id: rental.id } }) await AccountingService.recordRentalPayment(db, rental) } }, { connection: redis, concurrency: 5 })// Scheduler registers daily cron// jobs/scheduler.tsawait billingQueue.add('daily-billing', { trigger: 'cron' }, { repeat: { pattern: '0 0 * * *' } } // midnight daily) # 5. Bun Runtime Bun is a fast JavaScript/TypeScript runtime that executes TypeScript natively without a compilation step. For development this means running the backend directly from .ts files. For self-hosted deployment it compiles to a single standalone binary. ## 5.1 Development # Run backend directly — no tsc step neededbun run src/main.ts# Watch mode — restarts on file changesbun --watch run src/main.ts# Run testsbun test ## 5.2 Self-Hosted Binary Compilation # Compile to standalone binary# No Bun or Node.js runtime required on target machinebun build src/main.ts \ --compile \ --outfile platform-api \ --target bun-linux-x64 # or bun-linux-arm64# Result: single ~50-80MB executable# Ships inside Docker image — no source code included# Multi-arch build in CI:bun build src/main.ts --compile \ --outfile platform-api-amd64 --target bun-linux-x64bun build src/main.ts --compile \ --outfile platform-api-arm64 --target bun-linux-arm64 ## 5.3 Docker Image # DockerfileFROM oven/bun:1 AS builderWORKDIR /buildCOPY packages/shared ./packages/sharedCOPY packages/backend ./packages/backendRUN cd packages/backend && \ bun install --frozen-lockfile && \ bun build src/main.ts \ --compile --outfile /build/platform-api# Runtime image — binary only, no sourceFROM ubuntu:24.04RUN apt-get update && apt-get install -y \ ca-certificates && rm -rf /var/lib/apt/lists/*COPY --from=builder /build/platform-api /app/platform-apiEXPOSE 8000CMD ["/app/platform-api"]# Result:# No TypeScript source in image# No Bun runtime in final image# No node_modules# Just the compiled binary + Ubuntu base # 6. Drizzle ORM Drizzle defines the database schema in TypeScript. All queries are fully typed — TypeScript knows the shape of every query result. Migrations are generated automatically from schema changes. ## 6.1 Typed Queries // Fully typed — TypeScript infers return type from schemaconst rental = await db.query.rentals.findFirst({ where: and( eq(rentals.id, id), eq(rentals.company_id, companyId) // multi-tenant scoping ), with: { account: { with: { students: true } }, inventoryUnit: { with: { product: true } } }})// TypeScript knows: rental.account.students[0].first_name// No any, no casting, no guessing// Update with type safetyawait db.update(rentals) .set({ status: 'returned', // TypeScript enforces enum rto_equity_accumulated: '450.00' }) .where(eq(rentals.id, id)) ## 6.2 Migrations # Generate migration from schema changesbunx drizzle-kit generate# Apply migrationsbunx drizzle-kit migrate# Migrations are plain SQL files — readable and reviewable# /packages/backend/src/db/migrations/# 0001_initial_schema.sql# 0002_add_repair_parts.sql# 0003_add_lesson_plans.sql# Applied automatically on startup in production# Self-hosted: runs during update process # 7. Complete Tech Stack Layer Technology Notes Language TypeScript 5.x All packages — API, desktop, mobile, web Runtime Bun Native TS execution, compile-to-binary for self-hosted API Framework Fastify Fast, TypeScript-first, schema validation built in ORM Drizzle ORM TypeScript schema, fully typed queries, SQL-transparent Validation Zod Shared schemas — frontend and backend use same definitions Job Queue BullMQ Redis-based, TypeScript-native, retry logic built in Database PostgreSQL 16 Aurora on SaaS, Docker container on self-hosted Cache / Queue broker Redis 7 Session cache, BullMQ broker, rate limiting Desktop Electron + React Windows/Mac/Linux, imports from shared package Mobile React Native iOS, Stripe Terminal Bluetooth, imports from shared Web portals React + Vite Customer portal + admin panel PDF generation Puppeteer HTML templates rendered to PDF for invoices/reports Auth Clerk or Auth0 JWT-based, RBAC, handles MFA Payments — Stripe stripe npm package Official TypeScript SDK Payments — GP globalpayments-sdk Node.js SDK Monorepo Turborepo Build pipeline, caching, workspace management Testing Vitest Fast, native ESM, compatible with Bun Tray app Go System tray manager for self-hosted — tiny binary Installer PowerShell → Inno Setup Phase 1: PS1 script. Phase 2: .exe wrapper. # 8. CI/CD Pipeline GitHub Actions handles the full build and release pipeline. SaaS deployments push to EKS. Self-hosted releases push compiled binaries to AWS ECR. On push to main: 1. Typecheck all packages (tsc --noEmit) 2. Lint (ESLint) 3. Test (vitest) 4. Build shared package 5. Bun compile backend → binary (amd64 + arm64) 6. Docker build → push to ECR registry.platform.com/platform:sha-abc123 registry.platform.com/platform:latest 7. Update version registry APIOn tag (release): Same as above plus: 8. Tag Docker image with version registry.platform.com/platform:1.6.0 9. Build Electron desktop app (auto-updates via electron-updater) 10. Build React Native iOS app (TestFlight distribution) 11. Deploy SaaS to EKS (rolling update) 12. Update changelog and release notes # 9. Local Development Setup # Prerequisites: Bun, Docker Desktop, Node.js (for tools)# Clone and installgit clone https://github.com/lunarfront/platformcd platformbun install# Start dependencies (Postgres + Redis)docker compose -f docker-compose.dev.yml up -d# Run database migrationscd packages/backendbunx drizzle-kit migrate# Start all packages in dev modebun run dev # Turborepo starts: # backend localhost:8000 # admin localhost:3001 # web-portal localhost:3002 # desktop Electron window# Run testsbun testbun test --watch