Skip to content

Emp1500/cryptopulse-api

Repository files navigation

CryptoPulse Logo

CryptoPulse

Production-grade cryptocurrency dashboard with real-time market data, portfolio tracking, and analytics

CI/CD Tests Node.js Express Supabase Deployed on Vercel License

Live Demo · Report Bug · Request Feature


Overview

CryptoPulse is a full-stack web application that gives investors a real-time, unified view of the cryptocurrency market. It combines live price data from CoinGecko with a personal portfolio tracker, interactive analytics charts, and a watchlist — all behind a secure, session-based authentication system backed by PostgreSQL.

Built with a production-grade stack: rate limiting, CSP headers, bcrypt password hashing, structured logging, Sentry error monitoring, a 40-test automated suite, and a GitHub Actions CI/CD pipeline that gates every deploy behind passing tests.


By the Numbers

Metric Value
Cryptocurrencies tracked (Markets page) 100 (top by market cap)
Cryptocurrencies on Homepage 12 (top by market cap)
Coin search index (for Add Holding / Watchlist) 250 coins, returns top 8 matches
API cache TTL — homepage & markets 5 minutes
API cache TTL — coin search index 30 minutes
Portfolio dashboard tabs 4 (Overview, Holdings, Analytics, Watchlist)
Performance chart time ranges 3 (7D, 30D, 90D)
Chart palette (allocation / analytics) 8 distinct colours
Quantity precision (holdings) Up to 8 decimal places
Auth rate limit 10 requests / 15 minutes per IP
bcrypt cost factor 10
Session cookie lifetime 7 days
Sentry trace sampling — production 20%
Automated tests 40 across 5 suites
Production dependencies 13
Dev dependencies 3
CI jobs 2 (test → deploy, gated)

Table of Contents


Features

Market Intelligence

Feature Detail
Live market data Top 12 coins on the homepage; top 100 coins on the Markets page, ordered by market cap
Server-side caching 2 independent caches — homepage/markets: 5-min TTL; coin search index: 30-min TTL. Stale-cache fallback on API outages
Coin search Searches a 250-coin index, returns the top 8 matches by name or symbol — no extra network call
Interactive charts Performance chart with 3 selectable time ranges (7D / 30D / 90D); allocation doughnut with 8-colour palette
Lottie animations 2 blockchain-themed animations (blockchain.json, network.json) served from cdn.jsdelivr.net (CSP-compliant)

Portfolio Dashboard — 4 tabs, zero page reloads

Tab What it shows
Overview 4 summary cards (total value, invested, P/L $, return %) + allocation doughnut + performance line chart
Holdings Full CRUD table — add via 250-coin search, edit, delete. Quantity displayed to 8 decimal places
Analytics Horizontal P/L bar chart per coin, mirrored allocation donut, best & worst performer cards
Watchlist Inline coin cards with live price + 24h % change; add/remove without leaving the page

Tab state is stored in the URL hash (#overview, #holdings, etc.) for direct-linkable deep navigation.

Authentication & Security

  • bcrypt password hashing — cost factor 10 (~100ms deliberate delay against brute force)
  • PostgreSQL session store (connect-pg-simple) — sessions survive Vercel serverless cold starts; 7-day cookie lifetime
  • req.session.save() wraps every post-auth redirect to guarantee session flush before response on serverless
  • Rate limiting10 requests / 15 minutes per IP on all /auth endpoints
  • Helmet CSP — strict scriptSrc, imgSrc, connectSrc whitelist; no unsafe-eval
  • express-validator sanitises and validates all form inputs server-side before any DB write

Observability

  • Winston structured logging across 3 levels (info / warn / error) with ISO timestamps on every request
  • Sentry error monitoring — 20% trace sampling in production, 100% in development; wired directly to the Express error handler

Tech Stack

Backend

Node.js Express 5 Supabase PostgreSQL bcryptjs Winston Sentry

Frontend

EJS Vanilla JS CSS Custom Properties Chart.js Bootstrap

Testing & DevOps

Jest Supertest GitHub Actions Vercel

External APIs

CoinGecko


Architecture

┌─────────────────────────────────────────────────────────────────┐
│                          Browser / Client                       │
│                  EJS templates + Vanilla JS                     │
└────────────────────────┬────────────────────────────────────────┘
                         │ HTTP
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Express 5.x  (app.js)                       │
│                                                                 │
│  Middleware stack:                                              │
│  Helmet (CSP) → express-session → user locals → routes          │
│                                                                 │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────┐     │
│  │ /auth       │  │ /portfolio   │  │ /api + page routes  │     │
│  │ login       │  │ dashboard    │  │ /api/markets        │     │
│  │ register    │  │ holdings     │  │ /markets  /about    │     │
│  │ logout      │  │ watchlist    │  │ /graphs   /news     │     │
│  └──────┬──────┘  └──────┬───────┘  └────────┬────────────┘     │
│         │                │                    │                 │
│  ┌──────▼────────────────▼────────────────────▼──────────────┐  │
│  │               isAuthenticated middleware                  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌─────────────────────────────────┐  ┌───────────────────────┐ │
│  │   In-memory cache (5-min TTL)   │  │  Winston logger       │ │
│  │   Stale-cache fallback on err   │  │  Sentry error handler │ │
│  └────────────────┬────────────────┘  └───────────────────────┘ │
└───────────────────│─────────────────────────────────────────────┘
                    │
        ┌───────────┴────────────┐
        │                        │
        ▼                        ▼
┌───────────────┐      ┌──────────────────────┐
│  CoinGecko    │      │  Supabase            │
│  API          │      │  (PostgreSQL)        │
│               │      │                      │
│  /coins/      │      │  users               │
│   markets     │      │  sessions            │
│  /simple/     │      │  portfolio_holdings  │
│   price       │      │  watchlist           │
└───────────────┘      └──────────────────────┘

Portfolio dashboard data flow

GET /portfolio
    │
    ├─ Promise.all([
    │      supabase: portfolio_holdings WHERE user_id
    │      supabase: watchlist         WHERE user_id
    │  ])
    │
    ├─ Promise.all([
    │      CoinGecko: /simple/price      (holdings enrichment)
    │      CoinGecko: /coins/markets     (watchlist enrichment)
    │  ])
    │
    └─ res.render('portfolio/dashboard', { holdings, watchlist, totals })
         └─ Client: pure JS tab switching (URL hash, no page reload)

Getting Started

Prerequisites

Tool Version
Node.js 22.x
npm 9.x+
Supabase account (for PostgreSQL)

Installation

# 1. Clone the repository
git clone https://github.com/Emp1500/cryptopulse-api.git
cd cryptopulse-api

# 2. Install dependencies
npm install

# 3. Configure environment variables (see below)
cp .env.example .env   # then fill in your values

# 4. Start development server
npm run dev

The app will be available at http://localhost:3000.


Environment Variables

Create a .env file in the project root:

# Supabase — production database
SUPABASE_URL=https://<your-project-id>.supabase.co
SUPABASE_SERVICE_KEY=<your-service-role-key>

# PostgreSQL connection string (for session store)
DATABASE_URL=postgresql://postgres:<password>@db.<project-id>.supabase.co:5432/postgres

# Session
SESSION_SECRET=<a-long-random-string>

# Error monitoring (optional)
SENTRY_DSN=<your-sentry-dsn>

# For running tests against an isolated DB
TEST_SUPABASE_URL=https://<test-project-id>.supabase.co
TEST_SUPABASE_SERVICE_KEY=<test-service-role-key>

Note: The test suite runs against a separate Supabase project to keep production data clean. CI injects TEST_SUPABASE_URL and TEST_SUPABASE_SERVICE_KEY via GitHub Secrets.

Required Supabase tables

-- Users
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Sessions (managed by connect-pg-simple)
CREATE TABLE sessions (
  sid VARCHAR NOT NULL PRIMARY KEY,
  sess JSON NOT NULL,
  expire TIMESTAMPTZ NOT NULL
);

-- Portfolio holdings
CREATE TABLE portfolio_holdings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  coin_id TEXT NOT NULL,
  symbol TEXT NOT NULL,
  name TEXT NOT NULL,
  image TEXT DEFAULT '',
  quantity NUMERIC NOT NULL,
  purchase_price NUMERIC NOT NULL,
  purchase_date DATE,
  notes TEXT DEFAULT '',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Watchlist
CREATE TABLE watchlist (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  coin_id TEXT NOT NULL,
  symbol TEXT NOT NULL,
  name TEXT NOT NULL,
  image TEXT DEFAULT '',
  added_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(user_id, coin_id)
);

API Reference

Authentication — /auth

Method Endpoint Body Response Description
GET /auth/login 200 HTML Login page
POST /auth/login email, password 302 /portfolio Authenticate user
GET /auth/register 200 HTML Register page
POST /auth/register name, email, password, confirmPassword 302 /auth/login Create account
POST /auth/logout 302 / Destroy session

Portfolio — /portfolio (authentication required)

Method Endpoint Body Response Description
GET /portfolio 200 HTML Dashboard (fetches holdings + watchlist in parallel)
GET /portfolio/holdings 200 JSON All holdings with live prices
POST /portfolio/holdings coinId, symbol, name, quantity, purchasePrice, purchaseDate? 200 JSON Add holding
PUT /portfolio/holdings/:id quantity?, purchasePrice?, purchaseDate? 200 JSON Update holding
DELETE /portfolio/holdings/:id 200 JSON Delete holding
GET /portfolio/watchlist 200 HTML Watchlist page
POST /portfolio/watchlist coinId, symbol, name, image? 200 JSON Add to watchlist
DELETE /portfolio/watchlist/:coinId 200 JSON Remove from watchlist

Public API & Pages

Method Endpoint Description
GET /api/markets Top 100 coins JSON (used by coin-search in modals)
GET / Homepage — top 12 coins by market cap
GET /markets Full markets table (top 100)
GET /graphs Price charts page
GET /about About page
GET /news Redirects to CoinMarketCap news

Project Structure

cryptipulse/
├── app.js                          # App entry point, middleware, page routes
├── vercel.json                     # Serverless deployment config
├── jest.config.js                  # Test runner config
├── package.json
│
├── config/
│   ├── database.js                 # Supabase client (prod/test env-aware)
│   └── logger.js                   # Winston logger (timestamps + levels)
│
├── middleware/
│   └── auth.js                     # isAuthenticated route guard
│
├── routes/
│   ├── api.js                      # GET /api/markets — CoinGecko proxy
│   ├── auth.js                     # Register, login, logout + rate limiting
│   └── portfolio.js                # Holdings + watchlist CRUD
│
├── views/
│   ├── index.ejs                   # Homepage (top 12 coins + Lottie animations)
│   ├── graphs.ejs                  # Price charts page
│   ├── portfolio/
│   │   ├── dashboard.ejs           # 4-tab portfolio dashboard
│   │   └── watchlist.ejs           # Standalone watchlist page
│   ├── auth/
│   │   ├── login.ejs
│   │   └── register.ejs
│   └── partials/
│       ├── header.ejs              # Sticky dark navbar (auth-aware)
│       ├── footer.ejs              # 4-column footer
│       ├── markets.ejs             # Top 100 markets table
│       └── about.ejs               # About page
│
├── public/
│   ├── css/
│   │   ├── theme.css               # Design token system (CSS custom properties)
│   │   ├── portfolio.css           # Dashboard, tab nav, modal styles
│   │   ├── about.css               # About page styles
│   │   ├── markets.css
│   │   └── other.css
│   ├── js/
│   │   ├── observer.js             # IntersectionObserver scroll animations
│   │   └── markets.js              # Markets table search + filter
│   └── animations/                 # Lottie JSON animation files
│
├── tests/
│   ├── helpers/
│   │   ├── db.js                   # Test factories: createTestUser, createTestHolding, createTestWatchlistCoin
│   │   └── setup.js
│   ├── integration/
│   │   ├── auth.test.js            # POST /auth/login, /register, /logout
│   │   ├── portfolio.test.js       # GET /portfolio, holdings CRUD, watchlist data
│   │   └── watchlist.test.js       # Watchlist add/remove/duplicate prevention
│   └── unit/
│       ├── auth.utils.test.js
│       └── portfolio.utils.test.js
│
└── .github/
    └── workflows/
        └── ci-cd.yml               # Test → Deploy gated pipeline

Testing

40 tests · 5 suites · 0 mocks — all run against an isolated Supabase test project (completely separate from production data).

# Run all 40 tests
npm test

# Watch mode (development)
npm run test:watch

# Run a single suite
npx jest tests/integration/auth.test.js

Coverage breakdown

Suite Tests What's covered
auth.test.js 7 Register, login (valid + wrong password + unknown email), duplicate email rejection, logout, session destruction
portfolio.test.js 10 Auth guard on GET /portfolio, watchlist data present in rendered HTML, holdings CRUD (add/update/delete), input validation (zero quantity, missing coinId)
watchlist.test.js 9 Add coin, remove coin, duplicate coin prevention (unique constraint), unauthenticated access guard
auth.utils.test.js 7 bcrypt hashing and comparison, session utility helpers
portfolio.utils.test.js 7 P/L calculation logic, live price enrichment, percentage return computation
Total 40

Engineering decisions

  • Real database, no mocks — Tests hit a live Supabase Postgres instance. Mocking the DB previously masked a migration bug where mocked tests passed but the production schema rejected the same query.
  • 3 factory helperscreateTestUser(), createTestHolding(), createTestWatchlistCoin() each insert a row and return the created record; afterEach deletes by user_id for clean state.
  • Supertest agent — Stateful request.agent(app) carries session cookies across multiple requests, enabling full end-to-end authenticated flows (login → portfolio → delete) in a single test.

CI/CD Pipeline

Every push to any branch triggers the test job. A deploy to Vercel production only fires if the tests pass and the branch is main.

git push origin main
       │
       └─▶ GitHub Actions
               │
               ├─▶ [test]    Node.js 22 · npm ci · npm test
               │       │
               │       ├── PASS ──▶ [deploy]  vercel --prod --yes
               │       │
               │       └── FAIL ──▶ Pipeline halts. No deploy.
               │
               └─▶ Vercel Production (https://cryptipulse.vercel.app)

GitHub Secrets required:

Secret Purpose
TEST_SUPABASE_URL Isolated test database URL
TEST_SUPABASE_SERVICE_KEY Test DB service role key
VERCEL_TOKEN Team-scoped Vercel deploy token
VERCEL_ORG_ID Vercel team ID
VERCEL_PROJECT_ID Vercel project ID

Security

Layer Implementation
Password hashing bcryptjs — cost factor 10 (~100ms per hash; deliberate slowdown against brute force)
Session storage PostgreSQL via connect-pg-simple — zero sensitive data client-side; 7-day cookie lifetime
Cookie hardening httpOnly: true, secure: true in production, sameSite default
Rate limiting 10 requests / 15 minutes per IP on all /auth endpoints (express-rate-limit)
Content Security Policy Helmet — strict scriptSrc (jsdelivr + cdnjs only), imgSrc (coingecko only), connectSrc: self; no unsafe-eval
Input validation express-validator — trim, normalise, and validate all form fields before any DB write
Session flush req.session.save() before every post-auth redirect — prevents lost-session race on Vercel serverless
Error monitoring Sentry — 20% trace sampling in production, 100% in development; wired to Express error handler

License

Distributed under the ISC License.


Built by Vedant Wagh

GitHub LinkedIn

About

Live Demo

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors