←All projects

Selected project / Engineering case study

Prism Store

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.

Prism API architecture diagram
API architecture: requests pass through rate limiting, CORS, and validation, then JWT and role guards, before reaching the domain modules.
Prism payment flow sequence diagram
Payment flow: the order is created with a PaymentIntent, the client confirms with Stripe, and a signed webhook updates the order.

03 / Technical decisions

Key technical decisions

01

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.

02

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.

03

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.

04

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.

05

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. 1

    Push to main

    Each repository's GitHub Actions pipeline starts on every push.

  2. 2

    Test

    192 tests for the API, 97 for the storefront. A failure stops the pipeline here.

  3. 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. 4

    Deploy

    Over SSH, with Docker Compose on the VPS.

  5. 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.