Full-stack e-commerce with Stripe payments, refunds, and an admin panel.
- Next.js
- NestJS
- PostgreSQL
- Stripe
- Docker
- GitHub Actions
- Prisma
- Nginx
- TypeScript
- Jest
- Vitest
01 / Overview
What it is
- Role
- Solo: API, storefront, and infrastructure
- Repositories
- Two: NestJS API and Next.js storefront
- Status
- Deployed on a hardened VPS
Prism is a full-stack e-commerce platform for smart gadgets and everyday tech essentials. It covers the whole shopping cycle: catalog discovery, cart, Stripe checkout, order tracking, and an admin panel to manage products, orders, users, and refunds.
It is split into two independent repositories: a RESTful NestJS API with PostgreSQL and Prisma, and a Next.js storefront that consumes it through a single typed client. Both run in production behind Nginx on a VPS I provisioned and hardened myself.
- 192
- API unit tests · 70.6% line coverage
- 97
- frontend tests across 10 files
- CI/CD
- tests, build, and deploy to a VPS with automatic rollback
- 3
- integrations: Stripe, Cloudinary, Resend
02 / Architecture
How it fits together
A look at the system architecture and how its components work together.
03 / Technical decisions
Key technical decisions
Payment state driven by signed, idempotent webhooks
The browser confirms the payment with Stripe Elements, but the order status only changes when the API receives a webhook from Stripe and verifies its signature. Stripe delivers events at least once, so the handler is idempotent: the webhook and the client confirmation converge on a single finalize step that checks the payment's status inside a database transaction. A replayed event leaves the stock, the order, and the confirmation email unchanged.
Alternative considered
Marking an order as paid because the browser says it succeeded.
Refunds that touch stock only after Stripe confirms
The API calls Stripe first. Only when the refund succeeds does a short database transaction mark the payment refunded, cancel the order, and restore stock. A slow response from Stripe can't hold database locks, and a refund started from the Stripe dashboard arrives through the webhook and ends in the same state. Refund handlers check the payment's status first, so a repeated event doesn't restore stock again.
Alternative considered
Holding a database transaction open across a call to a third-party API.
Checkout retries reuse the pending payment
When a customer retries, the API returns the existing PaymentIntent while it is still usable. A stale one is marked as failed before a new one is created, and an order that is already paid is rejected. Retrying checkout doesn't pile up PaymentIntents for the same order.
Alternative considered
Creating a new PaymentIntent on every attempt.
A layered request pipeline
Rate limiting, CORS, and input validation run first. JWT and role guards come next, and only then do requests reach the domain modules. Each concern lives in one place, so adding a module doesn't mean re-implementing security.
Alternative considered
Re-checking authentication and validation inside each controller.
One typed API client with transparent token refresh
The API issues short-lived access tokens and refresh tokens. On the frontend, every call goes through a single typed client that retries an unauthorized request once after refreshing the token, so components never deal with expired sessions or call fetch directly.
Alternative considered
Calling fetch from components and handling expired sessions in each one.
04 / Production
How it runs in production
Both repositories deploy to a VPS I provisioned and hardened myself. Nothing reaches production without passing its test suite, and a release that doesn't come up healthy reverts on its own.
Deploy pipeline
- 1
Push to main
Each repository's GitHub Actions pipeline starts on every push.
- 2
Test
192 tests for the API, 97 for the storefront. A failure stops the pipeline here.
- 3
Build
Versioned container images. The storefront uses Next.js standalone output with a three-stage Dockerfile, so the final image only contains what runs in production, as a non-root user.
- 4
Deploy
Over SSH, with Docker Compose on the VPS.
- 5
Verify
Healthchecks decide whether the release stays up.
Access
- Key-only SSH, root login disabled, non-default port
- Dedicated non-root deploy user
Network and edge
- Firewall allowing only 80, 443, and SSH, plus fail2ban
- Nginx terminates TLS with Certbot certificates, auto-renewed
- HTTP redirects to HTTPS, HSTS, TLS 1.2 and 1.3 only
Runtime
- Docker Compose with healthchecks, restart policies, and resource limits
- Internal networks, so Nginx is the only public entry point
System
- Automatic security updates
- Secrets live on the server, never in the repository
05 / What's included
What's included
Group 01
API
- JWT authentication with role-based access control
- Product and category management
- Cart, checkout, and orders with inventory updates
- Stripe PaymentIntents, webhooks, and refunds
- Cloudinary image uploads and Resend order emails
- Swagger/OpenAPI docs, validation, and rate limiting
Group 02
Storefront and admin
- Catalog with search, category filters, and pagination
- Accounts: profile, password, and order history
- Cart and Stripe checkout with order confirmation
- Admin panel for products, categories, orders, and users
- Image uploads and refund workflows
- Responsive UI with accessible controls
06 / Trade-offs
Trade-offs and next steps
Constraint 01
Access tokens are persisted in localStorage, which is simple but exposed to XSS.
Next step
Move them to httpOnly cookies.
Constraint 02
Rollback restores the previous images, not the database, so a release can't assume its schema change is reversible.
Next step
Write migrations so the previous release still works against the new schema.
Constraint 03
Production runs on a single VPS. That keeps operations simple and cheap, but there is no redundancy.
Next step
Managed container orchestration if real scale demands it.
Constraint 04
The tests target business logic and critical interactions instead of blanket coverage, which leaves the API at about 70% line coverage.
Next step
Extend coverage around payment edge cases first.