ContactHub Backend Service

Full-stack contact management engine live at contact-manager-fawn-alpha.vercel.app — Go clean architecture, bcrypt crypto, and partial unique indexing.

4 Layers
Clean Architecture
Handler → Service → Repo → Domain
Timing-Safe
Auth Defense
Constant-time dummy bcrypt verification
Partial Index
Data Isolation
Scoped WHERE deleted_at IS NULL
20 APIs
Shipped Endpoints
Full-stack CRUD, Auth, Admin & Health
01 / PROBLEM STATEMENT & OVERVIEW

Executive Architecture Overview

ContactHub is a production-grade REST API and full-stack contact management service (live at contact-manager-fawn-alpha.vercel.app). Built with Go (v1.25), Chi Router v5, PostgreSQL 15, and Docker. It provides secure multi-user contact management with strict user-level data isolation, soft deletion with a 30-day retention grace period, and clean separation of concerns across four decoupled layers.

The Core Engineering Problem

Many CRUD APIs leak multi-tenant boundaries when repository queries forget WHERE user_id = $1 filters, suffer from unique constraint collisions when users attempt to re-add previously soft-deleted phone numbers, leak account existence through login timing discrepancies, and cause slow request spikes by performing synchronous cascading deletes on user-facing paths.

02 / BLUEPRINT & FLOW

System Architecture & Data Pipeline

4-Layer Clean Architecture & Data IsolationUNIDIRECTIONAL DEPENDENCY
01 / TRANSPORT
Chi Router v5
JSON DTO decoding, route validation, and error envelope formatting.
Zero-alloc router
02 / BUSINESS
Service Layer
Argon2id crypto hashing, JWT token issue, and domain rule checks.
Memory-hard auth
03 / PERSISTENCE
Repository (SQL)
Parameterized queries enforcing WHERE user_id = $1 multi-tenancy.
Tenant isolation
04 / DATABASE
PostgreSQL 15
Partial unique index on (user_id, email) WHERE deleted_at IS NULL.
Safe soft-delete

Engineered with strict unidirectional dependency inversion: Transport Handlers decode HTTP requests → Service enforces domain business logic & security → Repository executes raw SQL queries strictly scoped to user_id → Domain models define pure Go business entities with zero framework dependencies.

LAYER 01

HTTP Transport Layer (chi router v5)

Zero-allocation routing, JSON DTO decoding and validation, route parameter parsing, and consistent JSON response envelope formatting.

LAYER 02

Service / Business Layer

Enforces authorization policies, orchestrates password hashing and timing-safe verification, manages JWT token lifecycles, and triggers domain state transitions.

LAYER 03

Repository Layer (database/sql)

Executes parameterized SQL queries with strict user_id scoping to guarantee multi-tenant data boundary isolation with zero ORM overhead.

LAYER 04

Domain Models Layer

Pure Go structs representing business entities with zero external HTTP router or database driver dependencies.

03 / DECISION LEDGER

System Design Trade-Offs

TRADE-OFF 01

Raw SQL (database/sql) vs Heavy ORM (GORM/Ent)

Engineering Rationale: Raw SQL gives full visibility over query execution plans, eliminates hidden N+1 query surprises, avoids heavy runtime reflection allocations, and explicitly takes advantage of PostgreSQL partial indexes and ON DELETE CASCADE constraints.

TRADE-OFF 02

Timing-Attack Mitigation on Account Enumeration

Engineering Rationale: When an unknown email attempts login, standard APIs return 401 immediately (taking ~2ms), while valid accounts incur a heavy ~70ms bcrypt hash comparison. ContactHub executes a dummy bcrypt comparison against a pre-computed hash when a user is not found, making response times indistinguishable and neutralizing enumeration attacks.

TRADE-OFF 03

Soft Deletion with Partial Unique Index vs Global Table Constraints

Engineering Rationale: A global UNIQUE constraint on phone prevents different users from saving the same contact and blocks a user from restoring a previously deleted contact. Using a composite partial index (user_id, phone) WHERE deleted_at IS NULL scopes uniqueness strictly per-user and strictly to active records.

TRADE-OFF 04

Ticker-Based Retention Reaper vs Synchronous Table Purge

Engineering Rationale: Soft-deleted contacts are assigned a purge_at timestamp (now + 30 days). A dedicated background worker goroutine periodically runs batch deletions (DELETE FROM contacts WHERE purge_at <= NOW()), offloading vacuum overhead and table locking delays from user-facing HTTP request paths.

TRADE-OFF 05

JWT Token Versioning vs Stateful Session Store

Engineering Rationale: To support instant session revocation upon password change without introducing a stateful Redis session dependency, users have a token_version column in PostgreSQL. JWT claims carry this version; changing a password increments the version, instantly invalidating all existing JWTs.

04 / STORAGE INTERNALS

Database Schema & Indexing Strategy

Normalized relational schema enforcing categories lookup, users authentication table, and contacts table with cascading foreign keys and partial unique indexes.

SQL DDL & PARTIAL INDEX DEFINITIONS
postgres-definition-1.sqlPostgreSQL 15
CREATE TABLE categories (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

INSERT INTO categories (name) VALUES ('General'), ('Family'), ('Friends'), ('Work'), ('College')
ON CONFLICT (name) DO NOTHING;
postgres-definition-2.sqlPostgreSQL 15
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
  is_verified BOOLEAN NOT NULL DEFAULT false,
  token_version INT NOT NULL DEFAULT 1,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
postgres-definition-3.sqlPostgreSQL 15
CREATE TABLE contacts (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  phone TEXT NOT NULL,
  email TEXT,
  category_id INTEGER NOT NULL DEFAULT 1 REFERENCES categories(id) ON DELETE RESTRICT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted_at TIMESTAMPTZ,
  purge_at TIMESTAMPTZ
);
postgres-definition-4.sqlPostgreSQL 15
CREATE UNIQUE INDEX contacts_user_phone_active_unique
ON contacts (user_id, phone)
WHERE deleted_at IS NULL;

CREATE INDEX idx_contacts_user_id ON contacts (user_id);
Indexing Strategy & Query Plan Optimization:Composite partial unique index contacts_user_phone_active_unique on (user_id, phone) WHERE deleted_at IS NULL guarantees that phone uniqueness is scoped to each user's active address book. Deleted contacts do not hold index locks, allowing immediate contact re-creation or restoration during the 30-day grace period.
07 / TECHNOLOGY LEDGER

Technology Stack Justifications

Go 1.25

Strong static typing, sub-millisecond route handling, native concurrency primitives, and clean standard library database interfaces.

Chi Router v5

100% net/http stdlib compatibility, zero allocations during routing, and composable middleware chaining.

PostgreSQL 15

Partial unique index capabilities, ON DELETE CASCADE foreign key lifecycles, and transactional DDL migrations.

Docker Compose

Orchestrates API backend and Postgres containers on a bridge network using Docker internal DNS (host=db) with automatic retry connection loops.

React & Next.js

Interactive responsive frontend deployed on Vercel communicating with containerized Go REST backend.

08 / INTERFACE CONTRACT

REST API Specification

MethodEndpoint PathSemantics & Status
POST/api/v1/auth/signupRegister new user account with verification token
POST/api/v1/auth/loginAuthenticate user and issue JWT bearer token
POST/api/v1/auth/forgot-passwordRequest password reset token with timing-safe enumeration protection
POST/api/v1/auth/reset-passwordReset password and increment token_version to revoke existing sessions
GET/api/v1/contactsList user-scoped contacts with search, category filter, and pagination
POST/api/v1/contactsCreate contact enforcing user-scoped phone uniqueness
DELETE/api/v1/contacts/:idSoft-delete contact setting deleted_at and purge_at (30-day retention)
POST/api/v1/contacts/:id/restoreRestore soft-deleted contact within 30-day retention window
GET/healthHealth check with 2s database ping verification
09 / RETROSPECTIVE & ROADMAP

Production Takeaways & Next Steps

Engineering Lessons

  • •Passing context.Context through all repository calls allowed clean database query cancellation when HTTP clients disconnect.
  • •Decoupling input DTOs from internal domain models prevented accidental leaks of private fields or sensitive user data.
  • •Mitigated timing attacks on user enumeration by executing dummy bcrypt comparisons when emails are not found.

Future Improvements

  • •Replace in-memory sliding rate limiting with Redis sliding-window ZSET for multi-replica horizontal scaling.
  • •Add Webhook push notifications for contact synchronization across external CRMs.
ELVINRODRIGUES.DEV
ERElvin Rodrigues

Backend Engineer focused on Go, PostgreSQL, Redis, and distributed systems.

CURRENTLY BUILDINGConcurrent Distributed Job Queue
Go + PostgreSQL FOR UPDATE SKIP LOCKED
AVAILABLE FOR
Backend Engineering · Distributed Systems · Infrastructure
Nitte, India · Relocation / Remote (2027)
© 2026 Elvin Rodrigues · Nitte, Karnataka, IN
Back to top