A Go REST API for link and bookmark management with JWT/API-key auth, Redis-backed rate limiting, and PostgreSQL full-text search.
┌─────────────────────────────────────────────────────────┐
│ LinkVault │
│ │
Client ──► Chi Router ──►│ Middleware Application │
(v5) │ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ Request ID │ │ Handlers │ │
│ │ Real IP │───────►│ ├── Auth │ │
│ │ Structured Log │ │ ├── Link │ │
│ │ Panic Recovery │ │ ├── API Key │ │
│ │ JWT / API Key │ │ └── Health │ │
│ │ Rate Limiter │ │ │ │ │
│ │ Gzip Compression │ │ ▼ │ │
│ └──────────────────┘ │ Services │ │
│ │ ├── AuthService │ │
│ │ ├── LinkService │ │
│ │ └── APIKeyService │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Repositories │ │
│ │ ├── UserRepo │ │
│ │ ├── LinkRepo │ │
│ │ └── APIKeyRepo │ │
│ └───────────┬───────────┘ │
└──────────────────────────────────────────┼──────────────┘
│
┌────────────┴────────────┐
│ │
PostgreSQL 16 Redis 7
(data + FTS) (rate limits)
- Dual authentication - JWT (access + refresh tokens) and revocable API keys (
X-API-Keyheader) - Full-text search - PostgreSQL-native
tsvectorsearch across link titles, descriptions, and tags - Rate limiting - Redis sorted-set sliding window, per-user, with
X-RateLimit-*response headers - Pagination and sorting - Cursor-free page/limit pagination with configurable sort fields
- Tag filtering - Filter saved links by tag
- Structured logging - JSON logs via zerolog with request ID correlation
- Graceful shutdown - Handles SIGINT/SIGTERM, drains in-flight requests
- Auto-migrations - SQL migrations run on startup
- Health endpoint - Reports status of API, database, and Redis
- Docker Compose - One command to run the full stack (app + Postgres + Redis)
- CI pipeline - GitHub Actions with lint, test (race detector), build, and Docker image verification
# Clone the repo
git clone https://github.com/shinegami-2002/linkvault.git
cd linkvault
# Start everything (app + PostgreSQL + Redis)
docker compose up --build -d
# Verify it's running
curl http://localhost:8080/health
# {"data":{"status":"ok","db":"ok","redis":"ok"}}The API is available at http://localhost:8080. Migrations run automatically on startup.
To stop:
docker compose down -v| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check (API, DB, Redis) |
GET |
/docs |
OpenAPI 3.0 spec (YAML) |
POST |
/api/v1/auth/register |
Register a new user |
POST |
/api/v1/auth/login |
Log in, receive JWT tokens |
POST |
/api/v1/auth/refresh |
Refresh access token |
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/links |
Create a link |
GET |
/api/v1/links |
List links (paginated, filterable by tag, sortable) |
GET |
/api/v1/links/search?q= |
Full-text search across links |
GET |
/api/v1/links/{id} |
Get a link by ID |
PUT |
/api/v1/links/{id} |
Update a link |
DELETE |
/api/v1/links/{id} |
Delete a link |
POST |
/api/v1/keys |
Create an API key (JWT only) |
GET |
/api/v1/keys |
List API keys (JWT only) |
DELETE |
/api/v1/keys/{id} |
Revoke an API key (JWT only) |
LinkVault supports two authentication methods. Both can be used on link endpoints; API key management requires JWT.
Register or log in to receive an access token and a refresh token:
# Register
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"securepassword123"}'
# Use the access token
curl http://localhost:8080/api/v1/links \
-H "Authorization: Bearer <access_token>"
# Refresh when expired
curl -X POST http://localhost:8080/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<refresh_token>"}'Create an API key (requires JWT), then use it via the X-API-Key header:
# Create key
curl -X POST http://localhost:8080/api/v1/keys \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"name":"CI Pipeline Key"}'
# Response includes raw_key -- save it, it is only shown once
# Use the key
curl http://localhost:8080/api/v1/links \
-H "X-API-Key: lv_abc123def456ghi789"Rate limiting uses a Redis sorted-set sliding window scoped per user. Default: 100 requests per 60 seconds (configurable via RATE_LIMIT and RATE_WINDOW env vars).
Every response includes rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1710000060
When the limit is exceeded, the API returns 429 Too Many Requests. If Redis is unavailable, the limiter fails open (requests are allowed).
linkvault/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint, wiring, graceful shutdown
├── internal/
│ ├── auth/
│ │ ├── jwt.go # JWT token creation and validation
│ │ ├── jwt_test.go
│ │ ├── apikey.go # API key hashing and verification
│ │ └── apikey_test.go
│ ├── config/
│ │ └── config.go # Env-based config (caarlos0/env)
│ ├── handler/
│ │ ├── handler.go # JSON response helpers
│ │ ├── auth.go # Register, login, refresh
│ │ ├── link.go # Link CRUD + search
│ │ ├── apikey.go # API key create, list, revoke
│ │ └── health.go # Health check
│ ├── middleware/
│ │ ├── auth.go # JWT + API key auth middleware
│ │ ├── auth_test.go
│ │ ├── ratelimit.go # Redis sliding-window rate limiter
│ │ ├── logging.go # Structured request logging
│ │ └── recovery.go # Panic recovery
│ ├── model/
│ │ ├── user.go # User model
│ │ ├── link.go # Link model
│ │ ├── apikey.go # API key model
│ │ └── response.go # Envelope, pagination, error types
│ ├── repository/
│ │ ├── user.go # User SQL queries
│ │ ├── link.go # Link SQL queries + full-text search
│ │ └── apikey.go # API key SQL queries
│ └── service/
│ ├── auth.go # Auth business logic
│ ├── link.go # Link business logic
│ └── apikey.go # API key business logic
├── migrations/
│ ├── 000001_init.up.sql # Schema creation
│ └── 000001_init.down.sql # Schema teardown
├── docs/
│ └── openapi.yaml # OpenAPI 3.0 specification
├── .github/
│ └── workflows/
│ └── ci.yml # Lint + test + build + Docker
├── Dockerfile # Multi-stage build (Alpine)
├── docker-compose.yml # App + PostgreSQL 16 + Redis 7
├── Makefile # Build, test, lint, migrate, docker
├── go.mod
└── go.sum
- Go 1.24+
- PostgreSQL 16
- Redis 7
- golangci-lint (for linting)
- Docker and Docker Compose (optional, for containerized setup)
# Set environment variables
export DATABASE_URL="postgres://linkvault:linkvault@localhost:5432/linkvault?sslmode=disable"
export REDIS_URL="redis://localhost:6379/0"
export JWT_SECRET="your-secret-here"
export PORT=8080
export RATE_LIMIT=100
export RATE_WINDOW=60
# Build and run
make runmake test
# Runs: go test -v -race -count=1 ./...make lint
# Runs: golangci-lint run ./...| Target | Description |
|---|---|
make build |
Compile binary |
make run |
Build and run locally |
make test |
Run tests with race detector |
make lint |
Run golangci-lint |
make migrate-up |
Apply database migrations |
make migrate-down |
Roll back database migrations |
make docker-up |
Start full stack via Docker Compose |
make docker-down |
Stop and remove containers + volumes |
| Component | Technology |
|---|---|
| Language | Go 1.24 |
| Router | chi v5 |
| Database | PostgreSQL 16 (full-text search via tsvector) |
| Cache / Rate Limit | Redis 7 (sorted-set sliding window) |
| Auth | JWT (golang-jwt v5) + bcrypt API keys |
| Logging | zerolog (structured JSON) |
| Config | caarlos0/env (env vars) |
| Testing | Go standard testing + testify |
| CI | GitHub Actions (lint, test, build, Docker) |
| Container | Multi-stage Docker build (Alpine) |
| API Spec | OpenAPI 3.0 |
MIT