8000 GitHub - WomB0ComB0/portfolio: 2025 - present portfolio ✨ · GitHub
[go: up one dir, main page]

Skip to content
Public template

Latest commit

 

History

975 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mikeodnis.dev

Personal portfolio, telemetry dashboard, and API hub for Mike Odnis (WomB0ComB0), live at mikeodnis.dev.

Version Next.js React Bun Elysia TypeScript License

Contents


Architecture

The application runs as a single Next.js 16 deployment on Vercel, splitting UI rendering from external API orchestration:

  1. Presentation Layer (src/app/(routes)): React 19 Server and Client Components styled with Tailwind CSS v4 and Radix UI primitives. Client data fetching uses a custom DataLoader built on @effect/platform, effect/Schema, and @tanstack/react-query (useSuspenseQuery).
  2. API Layer (src/app/api): Two Elysia.js routers mounted inside Next.js App Router catch-all handlers (/api/[[...route]] and /api/v1/[[...route]]). Before routing, each request passes a guard that checks the client IP against the IP/CIDR ban list and per-route rate limits kept in Upstash Redis (see Rate Limits & Bans); routes also set security headers and validate input at runtime.
  3. Content & Persistence:
    • Sanity CMS v6: Structured content for projects, work experience, certifications, places, talks, presentations, articles, and resume metadata.
    • Upstash Redis: API rate-limit counters and the IP/CIDR ban list, keyed by client IP address, for abuse prevention.
    • Firebase v13: Authentication and real-time storage for the /guestbook route.
  4. Observability: OpenTelemetry instrumentation (src/instrumentation.ts), Sentry error tracking, and Better Stack uptime monitoring and log ingestion.
graph TD
    subgraph "Client & Edge"
        Browser["Browser / PWA Service Worker"]
        DataLoader["Effect DataLoader + TanStack Query"]
    end

    subgraph "Next.js 16 App Router"
        RSC["Server Components & Route Groups"]
        CoreAPI["Elysia Core (/api/*)"]
        V1API["Elysia v1 (/api/v1/*)"]
    end

    subgraph "State & Storage"
        Upstash[("Upstash Redis")]
        Sanity[("Sanity CMS v6")]
        Firebase[("Firebase Auth & DB")]
    end

    subgraph "Upstream Providers"
        Spotify{{"Spotify Web API"}}
        GitHub{{"GitHub GraphQL API"}}
        WakaTime{{"WakaTime & LeetCode"}}
        Lanyard{{"Discord / Lanyard"}}
        Writing{{"Hashnode & Dev.to"}}
        Google{{"GA4 & YouTube Data API"}}
    end

    Browser --> RSC
    Browser --> DataLoader
    DataLoader --> CoreAPI
    DataLoader --> V1API
    CoreAPI --> Upstash
    V1API --> Upstash
    V1API --> Sanity
    V1API --> Firebase
    V1API --> Spotify
    V1API --> GitHub
    V1API --> WakaTime
    V1API --> Lanyard
    V1API --> Writing
    V1API --> Google
Loading

Tech Stack

Category Technology Usage
Runtime & Package Manager Bun (>=1.0.0), Node.js (>=24.15.0) Dependency management, script execution, and local dev runtime
Framework Next.js 16, React 19 App Router, Server Components, dynamic code splitting, and PWA support
API Engine Elysia.js 1.4, @elysiajs/eden Type-safe REST handlers, OpenTelemetry spans, Server-Timing headers, and CORS
Data Pipeline & Validation Effect (@effect/platform, effect/Schema), Zod 4, ArkType Typed HTTP client pipelines, retry policies, and strict schema decoding
State & Caching TanStack React Query v5, Jotai 3, Upstash Redis Suspense query caching, atomic client state, and server-side API rate limits and ban list
Styling & Motion Tailwind CSS v4, Radix UI, Motion 14, Paper Shaders Component primitives, animations, and WebGL background shaders
CMS & Database Sanity v6 (next-sanity), Firebase v13 (firebase, firebase-admin 14) Portfolio content management and authenticated guestbook entries
Observability Sentry 11, OpenTelemetry, Better Stack (@logtail/node) Distributed tracing, error reporting, and /api/status uptime aggregation
Code Quality & Testing Biome 2, TypeScript 6, Vitest 5, Playwright Formatting, linting, static type checking, unit tests, and Chromium E2E tests

Routes & Pages

Route Source Description
/ src/app/(routes)/page.tsx Home view with interactive canvas, command palette (Cmd+K), and quick links
/projects, /projects/[id] src/app/(routes)/(main)/(professioal)/projects Full project catalog and individual project case studies from Sanity
/experience, /experience/[id] src/app/(routes)/(main)/(professioal)/experience Career timeline and role details
/certifications src/app/(routes)/(main)/(professioal)/certifications Technical certifications and credentials
/resume src/app/(routes)/(main)/(professioal)/resume Interactive PDF resume viewer with Sanity-backed metadata
/media src/app/(routes)/(dev)/media Unified feed of Hashnode and Dev.to blog posts, talks, presentations, and YouTube videos
/stats src/app/(routes)/(dev)/stats Live engineering telemetry: GitHub contributions, WakaTime hours, LeetCode stats, Discord status, and site analytics
/spotify src/app/(routes)/(main)/(personal)/spotify Real-time Spotify playback, top tracks, and top artists
/places, /places/[placeId] src/app/(routes)/(main)/(personal)/places Curated map and directory of favorite locations using @vis.gl/react-google-maps
/guestbook src/app/(routes)/(main)/(personal)/guestbook Authenticated visitor messages backed by Firebase
/sponsor src/app/(routes)/(dev)/sponsor GitHub Sponsors tiers, current backers, and contribution links
/privacy, /cookies, /licenses src/app/(routes)/(legal) Legal and open-source license disclosures

API Reference

Core Endpoints (/api/*)

Method Path Description
GET /api/health Liveness check with uptime and timestamp
GET /api/status Service health aggregated from Better Stack Uptime monitors or status pages
POST /api/admin/ban Bearer-authenticated IP and CIDR ban management (ADMIN_API_TOKEN); the JSON body's action picks ban, unban, slow, list, etc., and an optional seconds makes a ban, slow-mode or CIDR entry temporary

v1 Endpoints (/api/v1/*)

Method Path Upstream Source Description
GET /api/v1/now-playing Spotify Web API Current playback state, track metadata, and album art
GET /api/v1/top-tracks Spotify Web API Short-term top tracks with resilient pagination fallback
GET /api/v1/top-artists Spotify Web API Short-term top artists and genres
GET /api/v1/github-stats GitHub GraphQL API Repositories, stars, followers, and contribution calendar
GET /api/v1/github-sponsors GitHub GraphQL API Active and past GitHub Sponsors and tier metadata
GET /api/v1/wakatime WakaTime API Total coding time and language breakdown
GET /api/v1/leetcode LeetCode GraphQL Solved problem counts by difficulty
GET /api/v1/lanyard Lanyard API Live Discord presence and custom activities
GET /api/v1/blog Hashnode & Dev.to Merged and chronologically sorted blog articles
GET /api/v1/google Google Analytics / YouTube Site pageviews and YouTube channel videos
POST /api/v1/messages Firebase Admin Sign the guestbook; needs a Firebase ID token (see Guestbook posts)
GET /api/v1/sanity/* Sanity CMS Sub-routes: /resume, /experiences, /projects, /projects/featured, /certifications, /places, /presentations, /talks, /articles, /videos

Rate Limits & Bans

Requests to /api/* and /api/v1/* pass an Elysia onRequest guard (src/lib/security/api-guard.ts) before routing. /api/health, CORS preflights and the separate /api/og image route are not guarded. Limits are sliding windows per client IP (IPv6 clients per /64 network):

Route class Limit Ban list applies
POST /api/v1/messages (signing the guestbook) 5 posts per 10 min Yes
Other /api/v1/* 60 requests per 60 s Yes
/api/admin/* 5 requests per 10 s No
Other /api/* 30 requests per 60 s Yes
IPs in slow mode 1 request per 30 s Yes
  • Banned IPs and IPs inside a banned CIDR range receive 403; clients over a limit receive 429 with Retry-After and RateLimit-* headers. Both responses are sent with Cache-Control: no-store.
  • Ban-list entries are sorted sets in Upstash Redis scored by their expiry time; permanent entries use a far-future score. Expired entries are ignored by the guard and deleted from every list whenever the ban API adds, removes or lists entries.
  • The ban-list read and the rate-limit counter run as one parallel Upstash round trip with a 300 ms budget. An in-process limit of 100 requests per minute per instance also applies.

Guestbook posts

POST /api/v1/messages signs the guestbook. The /guestbook page lists the messages on the server, so there is no GET.

  • Request: Authorization: Bearer <Firebase ID token> (from the signed-in visitor, a guest included) and a JSON body { "message": "..." } of 1 to 280 characters.
  • Checks, in order: the guard above (bans, then the 5-per-10-minute guestbook limit), the body schema (400), the token (401), then the text itself (400 when nothing is left once markup is removed). Nothing is written until every check passes.
  • Token: verified on the server with jose, by Firebase's rules for third-party JWT libraries: an RS256 signature by one of Google's securetoken keys (named by the token's kid, fetched from Google's public JSON Web Key Set and cached), aud the project id, iss https://securetoken.google.com/<project id>, exp in the future, iat and auth_time in the past (10 s of clock skew allowed), and a non-empty sub (the uid). There is no revocation lookup. A missing, malformed, invalid or expired token gets 401 with { "error": "..." } and a WWW-Authenticate: Bearer realm="guestbook" header; the body is the same in every case. A refused token's error code and the claim at fault (nothing else) are logged as an error. When GCP_PROJECT_ID and NEXT_PUBLIC_FIREBASE_PROJECT_ID differ, every post gets a reported 500 instead of a silent 401. So does a failure to fetch Google's keys (unreachable, a timeout, or an answer that is not a key set), so the browser does not sign a valid visitor out.
  • Why not firebase-admin/auth: Next loads firebase-admin at runtime, outside the bundle, and its auth entry require()s jose (an ES module) through jwks-rsa. A Node runtime without require(esm) cannot load that, and because the whole /api/v1 app shares one module with firebase-admin, every v1 route (and /stats) then answers 500. jose imported directly is bundled into the route. firebase-admin is still used for the Firestore writes. A unit test fails if any source file names firebase-admin/auth.
  • Stored: the checked text as plain text, and the name their Google or GitHub sign-in carries. A guest, or a sign-in method the site does not offer, is stored as Anonymous. A name sent in the body is ignored. The name is the visitor's own Firebase profile name, which they can change, so the uid is what identifies a poster. The message collection is publicly readable, so the uid is not on the message: it is kept in a server-only document with the same id under artifacts/{appId}/private/data/messagePoster, written in the same batch, and never returned.
  • In the browser: the card sends the visitor's ID token. After a 401 it sends the post once more with a token Firebase refreshes on purpose (a 401 stores nothing); refused again, it signs the visitor out and asks them to sign in again, keeping their text.
  • Limits of the check: guest sign-in uses the public web config, so anyone can get a valid token; the sign-in check gives each post a uid, not spam resistance. The per-IP guestbook limit is the control against floods (while Redis is failing, the guard lets requests through and only the per-instance in-process limit applies).
  • Firestore rules (deployed in the Firebase console; not in this repo): the messages collection artifacts/{appId}/public/data/message must allow public read (the page loader reads it with the client SDK) and deny every client create, update and delete. Every other path, messagePoster included, must deny clients both reads and writes. The Admin SDK ignores rules, so the API still writes. If clients may write there, anyone can skip this API and its checks.

Quick Start

Prerequisites

  • Bun >= 1.0.0
  • Node.js >= 24.15.0

Setup

git clone https://github.com/WomB0ComB0/portfolio.git
cd portfolio
bun install
cp .env.example .env.local

Populate the required keys in .env.local (see Environment Variables), then start the development server:

bun dev
  • App: http://localhost:3000
  • API v1: http://localhost:3000/api/v1
  • Health: http://localhost:3000/api/health

Environment Variables

Validation is enforced at build and startup via src/env.ts (@t3-oss/env-nextjs). Copy .env.example to .env.local.

Required Variables

Variable Scope Description
CSRF_SECRET Server Secret key for CSRF token signing
ADMIN_API_TOKEN Server Bearer token for /api/admin/ban endpoints
BETTERSTACK_API_KEY Server Better Stack Uptime API key for /api/status
NEXT_PUBLIC_SANITY_PROJECT_ID Client Sanity CMS project ID (NEXT_PUBLIC_SANITY_DATASET defaults to production)
NEXT_PUBLIC_FIREBASE_* Client Firebase client config (API_KEY, AUTH_DOMAIN, PROJECT_ID, STORAGE_BUCKET, MESSAGING_SENDER_ID, APP_ID, MEASUREMENT_ID)
GCP_* Server GCP service account credentials (GCP_PROJECT_ID, GCP_PRIVATE_KEY_ID, GCP_PRIVATE_KEY, GCP_CLIENT_EMAIL, GCP_CLIENT_ID, GCP_CLIENT_X509_CERT_URL)
UPSTASH_REDIS_REST_URL Server Upstash Redis REST endpoint
UPSTASH_REDIS_REST_TOKEN Server Upstash Redis REST token (read/write; never expose it as NEXT_PUBLIC_)
NEXT_PUBLIC_DISCORD_ID Client Discord user ID for Lanyard presence
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY Client Liveblocks public key
NEXT_PUBLIC_SENTRY_DSN Client Sentry DSN for client/server error reporting

GCP_PROJECT_ID is also the project whose Firebase ID tokens POST /api/v1/messages accepts, so it must name the same Firebase project as NEXT_PUBLIC_FIREBASE_PROJECT_ID. Verifying tokens needs only that project id; the other GCP_* values are the service account the guestbook writes with.

Optional Integrations

Variable Scope Description
SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN Server OAuth credentials for /api/v1/now-playing, /top-tracks, and /top-artists
GITHUB_TOKEN Server GitHub PAT (read:user, read:org) for /api/v1/github-stats and /github-sponsors
WAKA_TIME_API_KEY Server WakaTime API key for coding telemetry
SANITY_API_TOKEN Server Sanity read token for private datasets or draft previews
YOUTUBE_SERVER_API_KEY Server YouTube Data API v3 key
BETTERSTACK_STATUS_PAGE_ID Server Optional specific Better Stack status page ID
BETTERSTACK_SOURCE_TOKEN, BETTERSTACK_INGESTING_HOST Server Optional Better Stack Telemetry (Logtail) source token and host for log shipping
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID Client Google Maps credentials for /places

Scripts & Tooling

# Development & Production
bun dev                        # Start Next.js development server
bun run build                  # Run security audit, bundle cost analysis, and production build
bun start                      # Start production server

# Code Quality & Verification
bun run lint                   # Check with Biome (biome ci) and Stylelint; fails on errors, writes nothing
bun run lint:fix               # Apply Biome and Stylelint fixes
bun run format                 # Format files with Prettier
bun run type                   # Run TypeScript compiler check (tsc --noEmit)
bun run test                   # Run unit tests via Vitest (vitest run)
bun run e2e:ci                 # Run Playwright E2E test suite on Chromium

# Bundle & Repository Maintenance
bun run analyze                # Build with @next/bundle-analyzer and report bundle sizes
bun run scripts:package-cost   # Measure dependency size impact
bun run scripts:check-unused   # Detect unused files and exports
bun run scripts:copyright      # Apply or verify Apache 2.0 license headers

Scope & Architecture Decisions

To keep initial page loads fast and avoid client bundle bloat, several previously listed roadmap items have been completed or intentionally excluded:

  • [x] Blog & Media Engine (Completed): Live at /media, /api/v1/blog, and /api/v1/sanity/*. Posts are fetched concurrently from Hashnode (blog.mikeodnis.dev) and Dev.to (mikeodnis), alongside talks, presentations, and videos from Sanity and YouTube. An in-repo MDX compiler (@next/mdx, shiki) was skipped to keep server bundles small.
  • [x] Sponsorship Page (Completed): Live at /sponsor with GitHub GraphQL integration at /api/v1/github-sponsors.
  • [ ] Interactive 3D Stats via Three.js / React Three Fiber (Excluded for Performance): /stats renders telemetry with 2D SVG and Recharts components. Pulling in three and @react-three/fiber adds ~150–250 KB of gzipped client JavaScript plus WebGL context overhead, degrading Largest Contentful Paint (LCP) and Total Blocking Time (TBT) on mobile devices.
  • [ ] Internationalization / i18n (Excluded for Performance): All portfolio content and technical articles are authored in English. Adding i18n routing middleware introduces per-request edge latency, duplicates route trees (/[locale]/...), increases client dictionary payloads, and fragments Redis/CDN cache keys.

License

Licensed under the Apache License, Version 2.0. See the license headers in source files or http://www.apache.org/licenses/LICENSE-2.0 for full terms.

Releases

Sponsor this project

Used by

Contributors

Languages

0