{"id":50371248,"url":"https://github.com/help-14/linkz","last_synced_at":"2026-05-30T07:02:59.225Z","repository":{"id":360015335,"uuid":"1247989346","full_name":"help-14/linkz","owner":"help-14","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-24T15:07:57.000Z","size":77,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T17:12:04.744Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Svelte","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/help-14.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-24T03:30:45.000Z","updated_at":"2026-05-24T15:08:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/help-14/linkz","commit_stats":null,"previous_names":["help-14/linkz"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/help-14/linkz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/help-14%2Flinkz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/help-14%2Flinkz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/help-14%2Flinkz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/help-14%2Flinkz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/help-14","download_url":"https://codeload.github.com/help-14/linkz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/help-14%2Flinkz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33682998,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T07:02:58.453Z","updated_at":"2026-05-30T07:02:59.219Z","avatar_url":"https://github.com/help-14.png","language":"Svelte","funding_links":[],"categories":[],"sub_categories":[],"readme":"# linkz — Seat Reservation Platform\n\nA public seat reservation demo built for the Linkz Senior Engineer technical assessment. Displays available seats, authenticates users, processes payments via Stripe Checkout, and finalizes reservations upon payment confirmation.\n\n## Quick start\n\n```sh\n# 1. Start Postgres\ndocker compose up -d\n\n# 2. Install dependencies\npnpm install\n\n# 3. Configure environment\ncp .env.example .env\n# Edit .env with your Stripe test key and a random BETTER_AUTH_SECRET\n\n# 4. Run migrations and seed data\npnpm run seed\n\n# 5. Start dev server\npnpm run dev\n```\n\nThe seed script creates one user (`a@a.vn` / `test`) and 4 seats (A, B, C available; D reserved).\n\n## User flow\n\n1. **Register / Login** — email/password auth with 90-day session expiry\n2. **Browse seats** — see live availability and pricing\n3. **Select a seat** — review details and start payment\n4. **Pay via Stripe** — redirected to Stripe-hosted Checkout (test mode)\n5. **Confirmation** — payment verified server-side, reservation finalized\n\n## Architecture\n\n### Stack\n\n| Layer     | Choice                                        | Rationale                                                                            |\n| --------- | --------------------------------------------- | ------------------------------------------------------------------------------------ |\n| Framework | SvelteKit 5                                   | Familiarity, file-based routing, isomorphic rendering                                |\n| Database  | PostgreSQL 16                                 | Industry standard for transactional workloads, row-level locking, strong consistency |\n| ORM       | Drizzle ORM                                   | Type-safe query builder, native BetterAuth adapter, lightweight                      |\n| Auth      | BetterAuth                                    | Feature-complete (sessions, email/password, CSRF), SvelteKit adapter                 |\n| Payments  | Stripe Checkout                               | Hosted UI eliminates PCI scope, test mode enables local dev                          |\n| Cache     | Redis                                         | Industry standard                                                                    |\n| Styling   | Tailwind CSS 4 + shadcn-svelte                | Utility-first, maintains design consistency with minimal custom CSS                  |\n| Infra     | Docker Compose (local), adapter-auto (deploy) | Zero-config local dev, flexible deployment target                                    |\n\n### Project structure\n\n```\nsrc/\n├── app.html                      # HTML shell\n├── app.d.ts                      # App.Locals type augmentation\n├── hooks.server.ts               # Session hydration per-request\n├── lib/\n│   ├── auth-client.ts            # BetterAuth browser client\n│   ├── utils.ts                  # cn() utility\n│   ├── components/ui/            # shadcn-svelte UI primitives\n│   └── server/\n│       ├── auth/auth.ts          # BetterAuth server instance\n│       ├── db/\n│       │   ├── index.ts          # Drizzle + postgres clients\n│       │   └── schema.ts         # user, session, account, seats, payments\n│       └── stripe/index.ts       # Stripe SDK init\n└── routes/\n    ├── +layout.svelte            # Nav header, session display, sign out\n    ├── layout.css                # Tailwind + theme variables\n    ├── (homepage)/               # Landing page\n    ├── login/                    # Email/password sign in\n    ├── register/                 # Email/password sign up\n    ├── reserve/\n    │   ├── +page.svelte          # Seat grid selection\n    │   └── [seatId]/\n    │       ├── +page.svelte      # Routes to ReservationForm / Invoice / UnavailableState\n    │       ├── ReservationForm.svelte\n    │       ├── Invoice.svelte\n    │       └── UnavailableState.svelte\n    └── payment/\n        ├── create/+server.ts     # POST handler — reserves seat, creates Stripe session\n        └── success/+page.server.ts  # Verifies payment, finalizes reservation\n```\n\n### Database schema\n\n**`user`** — BetterAuth-managed users with email + password hash.\n\n**`seats`** — Seats with `status` enum (`available | pending | reserved`). The `pending` state temporarily locks a seat during payment (15 min expiry) to prevent double-booking.\n\n**`payments`** — Payment records tracking the lifecycle (`pending → completed | failed | expired`). Each payment links a user to a seat at a point in time.\n\n### Reservation flow (detailed)\n\n```\nUser clicks \"Pay with Stripe\"\n        │\n        ▼\nPOST /payment/create\n  ├─ Check seat availability (non-transactional pre-check)\n  ├─ BEGIN transaction\n  │   ├─ Check for existing pending payment (same user + seat)\n  │   ├─ UPDATE seats SET status='pending', reserved_by=user.id\n  │   │   WHERE (status='available' OR status='pending' AND reserved_by=user.id)\n  │   │   └─ returns 0 rows → seat taken → rollback → redirect with error\n  │   └─ INSERT INTO payments (status='pending')\n  └─ COMMIT\n  ├─ stripe.checkout.sessions.create()\n  │   └─ on failure: mark payment 'failed', release seat, redirect with error\n  └─ Redirect to Stripe Checkout URL\n        │\n        ▼ (user completes payment on Stripe)\n        │\n        ▼\nGET /payment/success?session_id=...\n  ├─ stripe.checkout.sessions.retrieve(sessionId)\n  ├─ Validate:\n  │   ├─ payment_status === 'paid'\n  │   ├─ metadata.userId matches authenticated user\n  │   └─ client_reference_id references a valid pending payment\n  ├─ BEGIN transaction\n  │   ├─ UPDATE payments SET status='completed', completed_at=NOW()\n  │   └─ UPDATE seats SET status='reserved'\n  └─ Display invoice\n```\n\n## Design decisions and trade-offs\n\n### Microservices vs monolith\n\nFor the sake of this test, I'll use monolith architecture, but it is still support horizontal scaling by running multiple instant of this app connected to the same database and cache (eg Redis) to share the session data, it is production safe and ready.\n\nFor real world project, I'll use microservices architecture and split this into auth service (backend), payment service(backend), booking service(backend) and svelte frontend service. Each team can work on there own service.\n\nMicroservices architecture comes with a few trade off:\n\n- Massive development and operational complexity: Development time increase massively, took more time to setup workable service. Increase latency when using service to service call.\n- Increase operational cost: K8s, AWS ECS is expensive, network call still costly.\n\n### Local authentication flow vs identity provider\n\nRegarding authentication, I agree that for a production-scale business, using a managed identity provider such as Firebase Auth or Clerk is often the better strategic choice. In this project, I used Better Auth mainly to demonstrate, my intention was not necessarily to suggest storing credentials internally is always the best production decision. Using dedicated provider does reduce operational and security burden unless we have some custom login flow that we need to support like intergration with local goverment services. Better-Auth itself is very safe and mature so it is a good option to manage authentication flow.\n\nI'm using session cookie for this demo, but to add JWT for mobile is easy with Better Auth: https://better-auth.com/docs/plugins/jwt\n\n### Webhook-free payment confirmation\n\nStripe webhooks are the production-standard approach for payment confirmation. This project deliberately avoids them for local development simplicity — instead, it verifies the session on the redirect-back page using `stripe.checkout.sessions.retrieve()`. This introduces a synchronous dependency on the user completing the redirect, which means:\n\n- **Trade-off**: If the user closes their browser before the redirect completes, the payment may go through but the seat stays `pending` until the 15-min expiry.\n- **Mitigation**: Add Stripe webhooks + a background job that reconciles stale pending seats (see [Known limitations](#known-limitations)).\n- **Why acceptable here**: Test-mode assessment; webhooks require a public endpoint or tunnel (ngrok) which adds setup friction.\n\n### Optimistic seat locking with 15-min expiry\n\nSeats transition `available → pending → reserved` rather than going directly from `available → reserved`:\n\n- **Trade-off**: Adds complexity (expiry checks, timer display on UI) versus atomic reserve.\n- **Why worth it**: Prevents double-booking during the payment window. Without pending state, two users could both reach Stripe Checkout for the same seat, both pay, and the second would need a refund. The pending lock reduces this window to near-zero.\n\n### Database-level locking (optimistic via WHERE clause)\n\nThe seat reservation uses `UPDATE ... WHERE (status = 'available')` inside a transaction rather than `SELECT ... FOR UPDATE`:\n\n- **Trade-off**: If two concurrent requests target the same seat, only one `UPDATE`'s `returning` clause returns a row. The loser's transaction commits but the affected row count is 0.\n- **Why acceptable**: Lighter-weight than explicit row locks. For the expected concurrency (3 seats, low traffic), this is sufficient. At scale, `SELECT ... FOR UPDATE` or advisory locks would be preferred.\n\n### Why not Drizzle for payment queries\n\nThe payment module uses raw `postgres` tagged template literals alongside Drizzle ORM:\n\n- **Trade-off**: Inconsistent API surface, type-unsafe `as` casts.\n- **Why accepted**: Drizzle's transaction API (`db.transaction`) was [historically limited](https://github.com/drizzle-team/drizzle-orm/issues) for raw SQL interop at the time of writing. The `postgres` client provides a more expressive transaction API with direct row materialization. A production version would standardize on one approach.\n\n## Security considerations\n\n| Concern                   | Status  | Notes                                                                                                                                    |\n| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| Password hashing          | ✓       | BetterAuth uses bcrypt with cost factor 10                                                                                               |\n| Session expiry            | ✓       | 90 days as required                                                                                                                      |\n| CSRF                      | Partial | SvelteKit checks Origin/Referer headers for POST requests; no CSRF token on /payment/create                                              |\n| Rate limiting             | ✗       | No protection on login or payment endpoints — should be added before production                                                          |\n| Price manipulation        | ✓       | Seat price read from DB at checkout time, not from client input                                                                          |\n| Payment ownership         | ✓       | Stripe metadata.userId verified against authenticated session                                                                            |\n| Secrets in source         | ✓       | Credentials are environment-injected                                                                                                     |\n| Hardcoded dev credentials | ⚠️      | Seed user `a@a.vn` / `test` is a dev-only convenience; should use env-configurable credentials or require first-user setup in production |\n\n## Operational concerns\n\n### Monitoring \u0026 observability\n\nThe application emits [OpenTelemetry](https://opentelemetry.io/) traces for all SvelteKit lifecycle points (`handle`, `load`, form actions, remote functions) via `src/instrumentation.server.ts`. Traces are exported over OTLP/HTTP to a local [Jaeger](https://www.jaegertracing.io/) instance.\n\n**Start the stack** (Jaeger is included in `docker-compose.yaml`):\n\n```sh\ndocker compose up -d\n```\n\n**View traces**: http://localhost:16686 — select service `linkz`.\n\nThe OTLP endpoint defaults to `http://localhost:4318`. Override via env:\n\n```env\nOTEL_EXPORTER_OTLP_ENDPOINT=\"http://your-collector:4318\"\n```\n\nIn a production environment, also add:\n\n1. **Health endpoint** — `GET /health` returning DB connectivity, Stripe API reachability\n2. **Structured logging** — structured logger (pino/winston) capturing request IDs, error context, payment lifecycle events\n3. **Error tracking** — integration with Sentry or similar for unhandled rejections and Stripe API errors\n\n### Background job needed — stale payment expiry\n\nA pg-boss worker now runs the background payment cleanup/reconciliation path. It should be started in a separate terminal with `pnpm worker`.\n\nThe worker currently handles:\n\n- expiring stale pending payments every minute\n- releasing seats held by expired pending payments\n- retrying Stripe session reconciliation if the user never returns from Checkout or the success-page verification hits a transient Stripe error\n\nThe expiry job performs the equivalent of:\n\n```sql\nUPDATE payments SET status = 'expired'\nWHERE status = 'pending' AND expires_at \u003c NOW();\n\nUPDATE seats SET status = 'available', reserved_by = NULL, reserved_at = NULL\nWHERE id IN (\n  SELECT seat_id FROM payments WHERE status = 'expired'\n);\n```\n\nThe inline expiry check on `reserve/[seatId]` still exists as a fallback, but the worker is now the primary cleanup mechanism.\n\n### Database migrations\n\nDrizzle Kit manages schema evolution via SQL migration files in `drizzle/`. The current migrations are:\n\n- `0000` — Initial schema (user, session, seats without price_cents, payments)\n- `0001` — BetterAuth accounts table\n- `0002` — Session metadata (ipAddress, userAgent)\n- `0003` — Added price_cents to seats\n\n**Note**: Migration `0000` creates the `seats` table without a `price_cents` column, which is added later in `0003`. A production migration would be squashed into a single initial migration for cleanliness.\n\n## Improvements for production\n\n### Critical path\n\n- **Stripe webhooks** — Replace redirect-based verification with idempotent webhook handling for reliability\n- **Rate limiting** — Apply per-IP rate limits on auth and payment endpoints\n- **Background seat expiry** — Dedicated job to release stale pending seats\n\n### Data integrity\n\n- **Store price at purchase time** — Add `price_cents` to the `payments` table so invoices are immutable\n- **Unique constraint on seat labels** — Prevent duplicate seat names in the same venue\n- **Composite unique on pending payments** — Ensure at most one pending payment per user per seat at the DB level\n- **Standardize on Drizzle ORM** — Eliminate raw SQL to improve type safety and maintainability\n\n### UX\n\n- **Loading states** — Skeleton/shimmer UI during page transitions and payment verification\n- **Auto-polling for seat status** — Poll seat availability while viewing a pending seat\n- **Mobile breakpoints** — The seat grid works on all screen sizes but the layout could use more polish at narrow widths\n\n### Operations\n\n- **Health checks** — `/health` endpoint for load balancer / orchestrator\n- **Structured logging** — Request-scoped logging with correlation IDs\n- **Graceful Stripe API version management** — Extract the API version to an env variable or pin it explicitly in a comment\n\n## Stripe test mode\n\nSet your test key in `.env`:\n\n```env\nSTRIPE_SECRET_KEY=sk_test_...\n```\n\nUse Stripe test card `4242 4242 4242 4242` with any future expiry and any CVC.\n\nList of test cards: https://docs.stripe.com/testing\n\nRun the worker in a second terminal during local development:\n\n```sh\npnpm worker\n```\n\n## Environment reference\n\n```env\nDATABASE_URL=\"postgres://user:password@localhost:6432/linkz\"\nBETTER_AUTH_URL=http://localhost:5173\nBETTER_AUTH_SECRET=\"generate with: openssl rand -base64 32\"\nSTRIPE_SECRET_KEY=\"sk_test_...\"\nOTEL_EXPORTER_OTLP_ENDPOINT=\"http://localhost:4318\"  # optional, defaults to this value\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhelp-14%2Flinkz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhelp-14%2Flinkz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhelp-14%2Flinkz/lists"}