Skip to content

hassankhan2608/Trimora

Repository files navigation

Trimora β€” a minimal open-source URL shortener built with Go, React, and PostgreSQL

Trimora

A minimal, self-hosted URL shortener. Paste a URL, pick an optional alias, set an optional expiry, share something shorter.

Built with Go, React, and PostgreSQL. No accounts, no analytics, no clutter.

Quick Start Β Β·Β  Features Β Β·Β  Screenshots Β Β·Β  API Β Β·Β  Configuration Β Β·Β  Development

License: MIT Go React Vite TypeScript PostgreSQL Docker


✨ Features

  • πŸ”— Short links in one click β€” paste a URL, get a short link.
  • ✏️ Custom aliases β€” claim memorable names like trimora.app/launch-notes.
  • ⏳ Optional expiry β€” links can expire after 1h, 1d, 7d, or 30d. Default is forever.
  • ♻️ Smart de-duplication β€” the same URL with no alias and no expiry returns the same short code.
  • πŸͺΆ Calm, accessible UI β€” single page, mobile-first, warm editorial style, no tracking.
  • 🩺 Production-ready health checks β€” /livez and /readyz for Kubernetes / load balancers.
  • 🐳 One-command deploy β€” docker compose up, with optional bundled Postgres or your own (Supabase, Neon, RDS).
  • πŸ§ͺ Tested β€” Go unit tests, ESLint, strict TypeScript, Playwright UI checks.

πŸ“Έ Screenshots

Desktop Β· Empty form Desktop Β· Result with expiry Mobile
Trimora desktop UI showing URL shortener form with alias and expiry inputs Trimora desktop UI showing a generated short link with copy button and expiry pill Trimora responsive mobile layout with stacked form fields and short link result

πŸš€ Quick Start

Prerequisites: Docker or Go 1.25+ with Node.js 20+ and a PostgreSQL 14+ database.

🐳 Docker Compose (recommended)

Spin up the full stack β€” API, web UI, and a local Postgres β€” with one command:

git clone https://github.com/your-org/trimora.git
cd trimora
cp .env.example .env
docker compose --profile local-db up --build

Then open http://localhost:5173.

Want to point at an external database (Supabase, Neon, RDS)? Drop the --profile local-db flag and set DATABASE_URL in .env.

☁️ Docker + Supabase (or any managed Postgres)
cp .env.example .env
# Edit .env and set DATABASE_URL to your managed Postgres connection string.
# For Supabase, use the direct (non-pooling) connection on port 5432
# with sslmode=require.

docker compose up --build

Trimora opens persistent connections and runs CREATE TABLE IF NOT EXISTS on boot, which works best on the direct port (5432) rather than the pgbouncer pooler (6543).

πŸ› οΈ Local development (no Docker)

1. Start Postgres (any local Postgres works):

docker run -d --rm --name trimora-pg \
  -e POSTGRES_USER=trimora -e POSTGRES_PASSWORD=trimora -e POSTGRES_DB=trimora \
  -p 5432:5432 postgres:16-alpine

2. Start the Go API:

cd api
DATABASE_URL='postgres://trimora:trimora@localhost:5432/trimora?sslmode=disable' \
BASE_URL='http://localhost:8080' \
ALLOWED_ORIGINS='http://localhost:5173' \
PORT=8080 \
go run ./cmd/server

3. Start the Vite web app:

cd web
npm install
VITE_API_BASE_URL='http://localhost:8080' npm run dev

Open http://localhost:5173.


πŸ”Œ API Reference

Base URL: http://localhost:8080 (defaults; override with BASE_URL).

POST /api/shorten β€” create a short link

Request body

Field Type Required Notes
url string yes http:// or https://, max 2048 chars.
alias string no 3–32 chars, [A-Za-z0-9_-]. Reserved: api, livez, readyz, healthz, health, static, admin, assets, favicon.ico, robots.txt.
expires_in "1h" | "1d" | "7d" | "30d" no Omit for permanent links.

Example

curl -X POST http://localhost:8080/api/shorten \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com/long/article",
    "alias": "launch-notes",
    "expires_in": "1d"
  }'

Response β€” 201 Created

{
  "code": "launch-notes",
  "short_url": "http://localhost:8080/launch-notes",
  "url": "https://example.com/long/article",
  "expires_at": "2026-04-28T15:33:09+05:30"
}

Errors

Status When
400 Bad Request invalid URL, invalid/reserved alias, invalid expires_in
409 Conflict alias already taken
GET /{code} β€” follow a short link
curl -i http://localhost:8080/launch-notes
# HTTP/1.1 302 Found
# Location: https://example.com/long/article
Status When
302 Found redirect to the original URL
404 Not Found unknown code (HTML page for browsers, JSON for API clients)
410 Gone link expired (HTML page for browsers, JSON for API clients)
GET /livez, /readyz, /healthz β€” health checks
Endpoint Purpose
/livez Process is alive. No DB call. Use as Kubernetes liveness probe.
/readyz Database is reachable (PING with 2s timeout). Use as readiness probe.
/healthz Backwards-compatible alias for /livez.
curl http://localhost:8080/livez
# {"status":"ok"}

βš™οΈ Configuration

Trimora is configured entirely through environment variables. Copy .env.example to .env to get started.

Server (API)
Variable Default Description
PORT 8080 API listen port.
BASE_URL http://localhost:8080 Public origin used to build short_url in responses.
ALLOWED_ORIGINS http://localhost:5173 Comma-separated CORS allow-list.
DATABASE_URL required PostgreSQL connection string.
Database
# Local Postgres via docker compose --profile local-db
DATABASE_URL=postgres://trimora:trimora@db:5432/trimora?sslmode=disable

# Managed Postgres (Supabase, Neon, RDS, …) β€” sslmode=require
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DB?sslmode=require

The schema is auto-created on boot (links table + indexes). No migration tool required.

Web (Vite)
Variable Default Description
VITE_API_BASE_URL empty Absolute API URL for production builds. Leave empty in dev to use the proxy.
VITE_API_PROXY_TARGET http://localhost:8080 Vite dev-server proxy target for /api, /livez, /readyz.

🧱 Project Structure

Trimora/
β”œβ”€β”€ api/                     # Go backend
β”‚   β”œβ”€β”€ cmd/server/          # main entrypoint
β”‚   └── internal/
β”‚       β”œβ”€β”€ config/          # env loading
β”‚       β”œβ”€β”€ httpapi/         # handlers, router, CORS, branded 404/410
β”‚       β”œβ”€β”€ links/           # service + repository (database/sql + pgx)
β”‚       β”œβ”€β”€ shortcode/       # base62 short-code generator (crypto/rand)
β”‚       β”œβ”€β”€ storage/         # connection + schema bootstrap
β”‚       └── validate/        # URL, alias, expiry rules
β”œβ”€β”€ web/                     # Vite + React + TypeScript frontend
β”‚   └── src/
β”‚       β”œβ”€β”€ components/      # ShortenForm, Result
β”‚       β”œβ”€β”€ types/api.ts     # shared API types
β”‚       β”œβ”€β”€ api.ts           # tiny fetch client
β”‚       └── App.tsx
β”œβ”€β”€ docs/screenshots/        # README assets
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ README.md
└── LICENSE

πŸ§‘β€πŸ’» Development

Backend checks
cd api
gofmt -l .
go vet ./...
go test ./...
go build ./...
Frontend checks
cd web
npm run typecheck
npm run lint
npm run build
End-to-end UI testing (Playwright)

The web UI is verified manually with Playwright on both desktop (1280Γ—820) and mobile (390Γ—844) viewports. The screenshots in this README are captured headlessly via the same flow.


🧭 Design Choices

  • No accounts, no analytics, no dashboard. Trimora is a tool, not a product.
  • database/sql + pgx β€” no ORM, no migration framework. The schema is small enough to manage in code.
  • crypto/rand-backed base62 codes β€” short, URL-safe, collision-retried at the service layer.
  • Optional expiry is a single nullable expires_at column with an index. Expired links return 410 Gone.
  • Same URL = same short code when no alias and no expiry are requested, to avoid pointless duplicates.
  • Branded 404 / 410 pages for browsers, JSON for API clients β€” content-negotiated by Accept.

🀝 Contributing

Issues and pull requests are welcome. Please keep changes small and focused, and run the checks listed above before opening a PR.

πŸ“„ License

MIT Β© Trimora contributors

About

Minimal, self-hosted URL shortener built with Go, React, and PostgreSQL

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors