Skip to content

Repository files navigation

SocialPlatform

A hobby-focused social media backend built as a set of small, independent Go microservices behind an Nginx gateway, communicating over REST and asynchronous NATS events.

SocialPlatform lets people register, build profiles around their hobbies, follow each other, publish tagged posts with image/video media, chat in real time (direct and group chats), and receive a ranked feed (global popular+recent, prioritizing followed authors) and live notifications.


Table of Contents


Overview

Each capability is owned by a dedicated service with its own PostgreSQL database (database-per-service). Services never reach into one another's database; they collaborate two ways:

  • Synchronously via REST/JSON through the Nginx gateway.
  • Asynchronously via NATS core pub/sub — publishers emit events, interested services maintain their own local copies of whatever data they need.

This keeps services independently deployable and testable, and means a slow or down service degrades gracefully rather than cascading.


Tech Stack

Layer Technology
Language Go 1.22
HTTP router chi v5
Datastore PostgreSQL 16 (via pgx/v5), one DB per service
Cache / rate limiting Redis 7
Messaging NATS 2 (core pub/sub)
Object storage MinIO (S3-compatible) for media
Auth JWT (HS256, access/refresh) + Google OAuth2
Real-time WebSockets (gorilla/websocket)
Migrations golang-migrate v4
Metrics Prometheus + Grafana
Gateway Nginx
Orchestration Docker Compose

Architecture

                              ┌─────────────┐
                     :8080    │    Nginx    │   (single entry point)
        client ───────────────▶   gateway  │
                              └──────┬──────┘
             ┌───────────────┬───────┼───────┬───────────────┐
             ▼               ▼       ▼        ▼               ▼
        ┌─────────┐   ┌──────────┐  ...  ┌─────────┐   ┌──────────────┐
        │  auth   │   │  post    │       │  feed   │   │ notification │
        │  :8081  │   │  :8082   │       │  :8085  │   │    :8086     │
        └────┬────┘   └────┬─────┘       └────┬────┘   └──────┬───────┘
             │             │                  ▲               ▲
             │  publish    │  publish         │ subscribe     │ subscribe
             └─────────────┴──────────▶  ┌────┴───────────────┴────┐
                                         │          NATS           │
                                         └─────────────────────────┘
   each service ──▶ its own PostgreSQL   │   Redis   │   MinIO   │  (shared infra)

Services

Service Port Responsibility
auth 8081 Registration, login, JWT refresh, Google OAuth2, user profiles, follow graph. Publishes user.*.
post 8082 Posts (CRUD), comments, likes, tags + tag search. Publishes post.*, comment.created.
media 8087 Authenticated image, video and audio upload to MinIO with per-type MIME/size validation (images ≤10 MiB, video ≤100 MiB, audio ≤20 MiB).
chat 8083 Direct (deduped per pair) & group chats with text + up to 3 image/voice-note attachments per message, chat-list previews with unread counts, read receipts, and real-time WebSocket delivery (message/typing/presence/read). Publishes chat.message.sent.
feed 8085 Ranked feed (global popular+recent, strongly boosting followed authors) + hobby-based suggestions, built entirely from local projections fed by NATS.
notification 8086 Turns events into notifications (REST history + live WebSocket push).
marketplace 8089 Buyer-first ("reverse") marketplace: buyers post requests, sellers submit offers, buyer accepts + marks done, both leave reviews. Own DB; chat/notifications reuse existing services. Publishes offer.*, request.fulfilled, review.created.
ai 8088 Stateless LLM gateway owning the host Ollama. Serializes inference through a priority queue + single worker. Consumes ai.request, replies on the caller's result_subject. First consumer: post auto-tagging — a fast vision model (qwen3.5:0.8b) captions the image, then the smart model (gemma4:e4b) turns text + caption into tags (text-only, fast). Best-effort — being down never blocks callers.

Event-Driven Data Flow

NATS subjects are the contract between services. Core pub/sub is used (not JetStream); publishes are best-effort and never block a user request.

Subject Published by Consumed by Purpose
user.created / user.updated auth feed mirror user identity + hobbies
user.followed / user.unfollowed auth feed, notification follow graph + "new follower" notice
post.created post feed denormalize post into the timeline
post.liked / post.unliked post feed, notification like counters + "liked your post" notice
comment.created post feed, notification comment counters + "commented" notice
chat.message.sent chat notification "sent you a message" notice
offer.created / offer.accepted / offer.rejected marketplace notification "new offer" / "offer accepted" / "offer declined" notice
request.fulfilled / review.created marketplace notification "leave a review" / "review received" notice
ai.request any service ai generic LLM job (task, input, media_ids, result_subject, priority)
ai.result.post.tag ai post auto-tag result (ok, output.tags, optional output.embedding); post-service applies tags + stores embedding when present

Two consequences worth understanding:

  1. The Feed service owns projection tables (feed_users, feed_follows, feed_posts, feed_likes) that mirror just enough data to render and rank a feed. It answers GET /api/feed purely from its own database — no cross-service calls at read time. The default for_you scope ranks every post by follow_boost + log-scaled engagement − recency decay, so a user who follows nobody still sees popular/recent content while followed authors are strongly prioritized; ?scope=following restricts to own + followed authors, newest first.
  2. The Notification service subscribes directly to the source events and composes the human-readable message itself; there is no central "notification.create" producer.
  3. Core NATS is at-most-once. Fresh projection databases need a backfill/reseed operation from service-owned APIs or seed tooling; do not fix this with JetStream or cross-DB feed queries.

Repository Layout

SocialPlatform/
├── docker-compose.yml            # full stack (datastores, infra, 7 services, gateway)
├── docker-compose.dev.yml        # overlay: publishes service & infra ports to host
├── nginx/nginx.conf              # gateway routing (REST + WebSocket)
├── prometheus.yml                # scrape config (all services on :9091)
├── .env.example                  # configuration template
├── go.mod / go.sum               # single Go module: "socialplatform"
├── pkg/                          # shared libraries (no business logic)
│   ├── config/                   # env helpers (GetEnv / GetEnvInt / GetEnvDuration)
│   ├── db/                       # pgx pool + DSN + connect-with-retry
│   ├── httputil/                 # responses, errors, middleware, rate limiter, health
│   ├── jwt/                      # token manager (access/refresh token types)
│   ├── nats/                     # core pub/sub client
│   └── metrics/                  # Prometheus middleware + /metrics server
├── services/<name>-service/
│   ├── main.go                   # wiring: config, DB, migrations, routes, shutdown
│   ├── Dockerfile                # multi-stage build (built from repo root context)
│   ├── migrations/               # golang-migrate SQL
│   └── internal/<domain>/        # model, repository/service, handler (+ tests)
└── docs/
    ├── api/api_spec.md           # canonical REST + WebSocket contract (frontend source of truth)
    ├── api/swagger.yaml          # OpenAPI 3.0 artifact (secondary)
    └── plans/                    # the implementation plan this repo was built from

Getting Started

Prerequisites

  • Docker and Docker Compose v2
  • Go 1.22.x (only needed for running tests/builds outside containers)

Quick Start (Docker Compose)

# 1. Create your local environment file
cp .env.example .env

# 2. Build images and start the full stack
docker compose up -d --build

# 3. Wait for everything to report healthy
docker compose ps

The stack uses health checks and readiness gating: application services wait for their PostgreSQL / Redis / NATS dependencies to be healthy, and Nginx waits for all services. Once docker compose ps shows everything healthy, the API is live at http://localhost:8080.

A quick smoke test:

# Register a user (returns the user plus access/refresh tokens)
curl -s -X POST localhost:8080/api/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"a@example.com","username":"alice","password":"pass1234"}'

# Health of any service through its container
docker compose exec auth-service wget -qO- http://localhost:8081/health

Verify the headline features end-to-end (all through the gateway at :8080):

# Save a token from register/login, then:
TOKEN=<access_token>

# Ranked feed — content appears even before you follow anyone
curl -s localhost:8080/api/feed -H "Authorization: Bearer $TOKEN"
curl -s "localhost:8080/api/feed?scope=following" -H "Authorization: Bearer $TOKEN"

# Chat list with last-message preview + unread counts
curl -s localhost:8080/api/chats -H "Authorization: Bearer $TOKEN"

# Upload an image or a video (gateway allows up to 100 MiB)
curl -s -X POST localhost:8080/api/media \
  -H "Authorization: Bearer $TOKEN" -F file=@clip.mp4

# Live channels (token in the query string, not a header):
#   ws://localhost:8080/ws/chat/<chatID>?token=$TOKEN
#   ws://localhost:8080/ws/notifications?token=$TOKEN

See docs/api/api_spec.md for every request/response shape.

Tear everything down (containers + volumes are removed with -v):

docker compose down        # stop & remove containers
docker compose down -v     # also delete database/MinIO volumes

Environment Configuration

All configuration lives in .env (copied from .env.example). Key groups:

Variable group Example Notes
POSTGRES_USER / POSTGRES_PASSWORD socialplatform / changeme Shared superuser for every per-service DB.
*_DB_HOST / *_DB_NAME / *_DB_USER / *_DB_PASSWORD AUTH_DB_HOST=postgres-auth One block per service.
JWT_SECRET dev-secret-change-in-production Change in production.
JWT_ACCESS_EXPIRY / JWT_REFRESH_EXPIRY 15m / 168h Parsed as Go durations.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET Optional; enables Google OAuth2.
MINIO_* / MEDIA_BUCKET minioadmin / social-media MinIO credentials & bucket (auto-created).
NATS_URL nats://nats:4222 Eventing is best-effort if NATS is down.

Local Development

Run a single service against a throwaway PostgreSQL — useful for fast iteration:

# Start a local Postgres
docker run -d --name pg-dev \
  -e POSTGRES_USER=socialplatform -e POSTGRES_PASSWORD=changeme \
  -e POSTGRES_DB=auth_service -p 5432:5432 postgres:16-alpine

# Point the service at it. MIGRATIONS_PATH must be set when running outside Docker,
# because migrations are resolved relative to the working directory.
export AUTH_DB_HOST=localhost AUTH_DB_PORT=5432 \
       AUTH_DB_USER=socialplatform AUTH_DB_PASSWORD=changeme AUTH_DB_NAME=auth_service \
       JWT_SECRET=dev MIGRATIONS_PATH=services/auth-service/migrations

go run ./services/auth-service

Migrations run automatically on startup and are fatal on failure — a service never starts with a missing schema.

To expose service and infrastructure ports on the host (for debugging the full stack), add the dev overlay:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

Testing

# Unit tests across the whole module (fast; DB-backed tests are skipped)
go test -short ./...

# With the race detector (as CI runs it)
go test -short -race ./...

# Build everything
go build ./...

Repository integration tests are gated on a real database. Provide a DSN to enable them:

export TEST_DB_DSN="postgres://socialplatform:changeme@localhost:5432/auth_service?sslmode=disable"
go test ./services/auth-service/internal/auth/...

Every service follows the same testing approach: pure validation and service logic are unit-tested with mocks or nil-pool guards; database access is covered by TEST_DB_DSN-gated integration tests; WebSocket hubs have behavioral tests for register/broadcast/unregister.


Observability

Tool URL (default) Notes
Prometheus http://localhost:9090 Scrapes every service's /metrics on port 9091.
Grafana http://localhost:3000 Default login admin / admin.

Each service exposes custom metrics via a middleware: http_requests_total and http_request_duration_seconds, labelled by service, method, templated route, and status.


Service & Port Reference

Component Internal port Published in dev overlay
Nginx gateway 80 8080 (always)
auth-service 8081 8081
post-service 8082 8082
chat-service 8083 8083
feed-service 8085 8085
notification-service 8086 8086
media-service 8087 8087
marketplace-service 8089 8089
(every service) metrics 9091
Prometheus 9090 9090
Grafana 3000 3000
MinIO (API / console) 9000 / 9001 9000 / 9001
NATS (client / monitor) 4222 / 8222 4222 / 8222

API Documentation

The canonical, hand-maintained contract — every REST endpoint plus the full WebSocket event protocols — lives in docs/api/api_spec.md. This is the file the frontend builds against. An OpenAPI 3.0 artifact also exists at docs/api/swagger.yaml (kept in sync with contract changes).

A few conventions:

  • All responses are JSON wrapped in a { "data": ... } envelope; errors return { "error": "message" }.
  • Protected endpoints require Authorization: Bearer <access_token>.
  • Demo tokens are currently stored by the SPA in localStorage; production deployments should move refresh tokens to HttpOnly; Secure; SameSite=Lax cookies, keep access tokens in memory, and enforce CSP.
  • WebSocket endpoints (/ws/chat/{chatID}, /ws/notifications) authenticate via a ?token=<access_token> query parameter, because browsers cannot set headers on the WebSocket handshake. Chat frames are typed envelopes (message/typing/presence/read) — see the API spec.

Troubleshooting

Symptom Likely cause & fix
A service container restarts repeatedly Its database wasn't ready. The built-in connect-retry usually recovers; check docker compose logs <service>.
relation "..." does not exist Migrations didn't run. Outside Docker, set MIGRATIONS_PATH=services/<svc>/migrations.
go get fails with "requires go >= 1.2x" A dependency's latest release needs a newer Go. Pin a 1.22-compatible version (see go.mod).
Editing nginx/nginx.conf has no effect The single-file bind mount keeps the original inode after an overwrite. Run docker compose up -d --force-recreate nginx.
Feed/notifications look empty after an action NATS delivery is asynchronous; allow a moment for projections to update.
Feed is empty after recreating its database in an existing stack Core NATS does not replay old events. Re-run seed/backfill tooling or replay from service-owned exports; do not query another service's database directly.

Frontend

A React + TypeScript SPA (services/frontend-service/) is implemented and served through the Nginx gateway at the site root, consuming every service — auth, profiles, follow graph, posts/comments/likes/tags, media, real-time chat, feed + suggestions, and live notifications (REST + WebSocket).

Built with Vite, Tailwind CSS, and shadcn/ui (Radix primitives). The current UI uses a warm editorial shell; older cinematic-dark docs are historical and should not override the implemented component structure.

Redesign architecture

  • Responsive app shell (src/components/layout/) — Header, Sidebar (≥md), mobile BottomNav, and an inline RightRail (≥lg) hidden on chat routes.
  • Warm editorial visual language — HSL CSS-variable design tokens, restrained borders/shadows, compact cards, and framer-motion entrance/press animations (calmed under prefers-reduced-motion via MotionConfig reducedMotion="user").
  • Server-state via React Query (src/api/{queries,mutations}.ts) — infinite-scroll feeds/comments/notifications, optimistic like / follow / join / comment / mark-read, and live WebSocket messages/notifications merged straight into the query cache (deduped by id) with unread-badge invalidation.
  • Auth — email/password plus "Continue with Google" (/api/auth/google redirect → /auth/callback token hydration); auth routes render outside the shell.

Theming — next-themes + CSS-variable tokens

  • Provider src/components/ThemeProvider.tsx (attribute="class", defaultTheme="dark", enableSystem) with themes Light, Dark, System, Emerald, Rose. Every color is an HSL CSS variable in src/index.css under :root / .dark / .emerald / .rose, exposed as Tailwind semantic utilities. The theme switcher lives inline in the Sidebar user menu.
  • Rule: components use semantic token utilities only (bg-card, text-muted-foreground, …); never hardcode colors, so all themes work without touching components.

Internationalization — react-i18next + RTL

  • Config src/i18n/index.ts (i18next + language detector, fallbackLng:'en'). Locales src/i18n/locales/{en,fr,ar}.json are kept key-for-key in sync. applyDirection() flips <html dir="rtl"> for Arabic on load and on language change; layouts use logical Tailwind utilities (start/end, ps/pe, ms/me) so the sidebar, right rail, and bubbles mirror correctly in RTL. The 🌐 switcher is also in the Sidebar user menu.
  • Rule: no hardcoded user-facing strings — every label/placeholder/message is a t('ns.key') present in all locale files.

The original design + backend-contract document lives at docs/plans/plan_design.md. The frontend quality gate is npx tsc --noEmit + npm run build (plus npm test for the vitest suite), run from services/frontend-service/. With the stack running, open http://localhost:8080. For local UI development: cd services/frontend-service && npm install && npm run dev (Vite proxies /api and /ws to :8080).

Contributing

Working on this repository as an AI agent? Read AGENTS.md first — it documents the non-negotiable architectural rules and the development workflow this codebase was built with. For the upcoming UI work, also read docs/plans/plan_design.md.

About

A hobby-focused social media backend built as a set of small, independent microservices

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages