Open-Source Cold Email & Warmup Platform
Warmbly 是開源的冷郵件平台:溫暖您的收件箱、發送活動、並對每個回覆進行分流。可在我們的雲端運行或自行託管整個系統。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證A modern, high-performance, self-hosted URL shortener written in Go, backed by PostgreSQL or SQLite with optional Redis. Ships as a single static binary with custom QR codes, link rotation, local MaxMind GeoIP analytics, automatic phishing screening, and strict security hardening.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗start_at and end_at parameters, or automatic time-to-live (TTL) limits.Every link request undergoes security screening and database optimization before resolving:
graph TD
A[User requests short code /ABC123] --> B{Code exists?}
B -- No --> C[404 Not Found]
B -- Yes --> D{Enabled and within its schedule?}
D -- Disabled / expired / outside window --> E[410 Gone]
D -- Active --> F{Password protected?}
F -- Yes, not yet unlocked --> G[Prompt for the link password]
G -- Invalid --> G
G -- Valid --> H{Device-specific target?}
F -- No --> H
H -- iOS / Android --> I[Use the matching device URL]
H -- Neither --> J{Rotation targets set?}
J -- Yes --> K[Pick a rotation target at random]
J -- No --> L[Use the main URL]
I --> M{Destination on the blocklist?}
K --> M
L --> M
M -- Blocked --> N[403 Forbidden]
M -- Safe --> O[Record click: country, browser, platform, referrer]
O --> P{Preview mode?}
P -- Yes --> Q[Preview page: confirm before leaving]
P -- No --> R[Interstitial with a 5s countdown, then navigate]
The destination is re-checked against the blocklist on every request, not only at creation time, so a link whose target is added to a phishing feed later stops resolving immediately.
net/http routing and html/template rendering — no web frameworkpgx (robust relational store), Redis (shared rate limiting & geo cache), SQLite via the pure-Go modernc.org/sqlite driver (resilient local fallback, no cgo)/metrics (protect it with METRICS_TOKEN or a firewall if the app port is reachable)cmd/redrx/ Entrypoint, logging, background blocklist refresh
internal/config/ Environment configuration
internal/store/ Database access; schema matches the previous SQLAlchemy models
internal/security/ Werkzeug-compatible password hashing
internal/session/ Signed cookie sessions and CSRF tokens
internal/ratelimit/ Flask-Limiter-compatible limit parsing, memory and Redis backends
internal/safety/ Blocked-domain and phishing-feed enforcement
internal/geo/ MaxMind lookups and IP anonymisation
internal/qr/ QR rendering with colours and logo overlay
internal/shortcode/ Short code generation and validation
internal/web/ HTTP handlers, middleware, templates and static assets
Ensure safe execution by loading our production-ready image verified with strict security hashes:
docker-compose.ghcr.yml as your template:
services:
app:
image: ghcr.io/arumes31/redrx:latest@sha256:0e2de80198eda6ed1b5b601d4def4dff4ce5e47c6051a81876a4ed7b5addcbc5
ports:
- "5000:5000"
environment:
- SECRET_KEY=${SECRET_KEY:?generate with openssl rand -hex 32}
- DATABASE_URL=postgresql://redrx:${POSTGRES_PASSWORD:?}@db:5432/redrx_db
- BASE_DOMAIN=short.yourdomain.com
docker-compose up -d
docker-compose up --build
The application will boot and expose itself at http://localhost:5000.
The application container runs as UID/GID 10001:10001; it does not support
PUID or PGID overrides. A bind mount hides the ownership prepared in the
image, so existing host directories mounted at /app/data or /app/db must be
writable by that UID before the container starts:
Upgrades must also migrate an existing blocklist_data named volume once,
before starting the new container. Refreshes create and rename temporary files
in /app/data, so the mounted directory must be owned by UID/GID 10001:10001:
docker compose run --rm --no-deps --user 0:0 --cap-add CHOWN \
--entrypoint sh web -c 'chown -R 10001:10001 /app/data'
sudo chown -R 10001:10001 /srv/redrx/data /srv/redrx/db
sudo find /srv/redrx/data /srv/redrx/db -type d -exec chmod 0750 {} \;
sudo find /srv/redrx/data /srv/redrx/db -type f -exec chmod 0640 {} \;
Use only the paths you actually bind mount; /app/db is needed for SQLite, not
the PostgreSQL setup in the supplied Compose files. Create new directories with
the correct ownership so Docker does not create them as root:
sudo install -d -o 10001 -g 10001 -m 0750 /srv/redrx/data /srv/redrx/db
The GeoIP mount is read-only in the application container. If it is also a host
bind mount, its directories need execute/read permission and its database files
need read permission for UID 10001; keep ownership with the GeoIP updater
instead of changing it to the application user.
Requires Go 1.26.5 or newer, the minimum declared by the go directive in go.mod. Dependencies are pinned in go.mod and checksum-verified through go.sum.
go mod download
go build ./cmd/redrx
The server defaults to SQLite at db/shortener.db, so no external services are needed for development:
# Windows PowerShell
$env:SECRET_KEY = "dev-secret"
$env:REDRX_DEBUG = "true"
$env:BASE_DOMAIN = "localhost:5000"
go run ./cmd/redrx
# Linux / macOS
SECRET_KEY=dev-secret REDRX_DEBUG=true BASE_DOMAIN=localhost:5000 go run ./cmd/redrx
The application boots at http://localhost:5000. Setting REDRX_DEBUG (or the legacy FLASK_DEBUG) relaxes the canonical-domain redirect, allows a fallback SECRET_KEY, and drops the Secure flag from session cookies so plain HTTP works locally.
go test ./... # full suite
go test -race ./... # with the race detector
go vet ./... # static analysis
The suite includes a committed SQLite fixture written by the previous Python application (internal/store/testdata/legacy_python.db). Tests read and write it to prove that existing databases, password hashes and timestamp formats remain compatible.
| Category | Variable | Default | Description |
|---|---|---|---|
| Core | SECRET_KEY | - | Strong cryptographic key for session signing and hashing. Enforced in production; documented placeholder values are refused, and keys under 32 characters log a warning. Rotating it logs everyone out and invalidates stored TOTP secrets and recovery codes. |
| Core | ALLOW_WEAK_SECRET_KEY | false | Escape hatch that lets an existing deployment boot with a placeholder SECRET_KEY while it plans a rotation. |
| Domain | BASE_DOMAIN | short.example.com | Base host string used when formatting shortened URLs. |
| GeoIP | MAXMIND_LICENSE_KEY | - | Required to download the GeoIP dataset and update in background. |
| Phishing | ENABLE_PHISHING_CHECK | true | Enables domain protection against real-time blacklists. |
| Phishing | ENABLE_AUTO_REMOVE_PHISHING | false | Automatically removes links that redirect to verified phishing domains. |
| Database | DATABASE_URL | - | PostgreSQL URI (e.g. postgresql://user:pass@host:5432/db). Defaults to SQLite local file fallback. |
| Limits | RATELIMIT_STORAGE_URL | redis://redis:6379 | Rate limiting backend. Can fall back to local storage memory:// in dev. The compose files build it from REDIS_PASSWORD. |
| Limits | REDIS_PASSWORD | - | Compose only, required: the password the bundled Redis is started with (--requirepass). Keep it URL-safe. |
| Limits | RATELIMIT_LOGIN_ACCOUNT | 20 per hour | Per-account budget for the password and TOTP login steps, in addition to the per-IP . |
Rate limiting, analytics and the abuse controls all key off the client IP, so
redrx has to be told who is allowed to tell it what that IP is. By default it
trusts nobody and uses the peer address, which is correct for a directly
exposed service: a client that can set its own X-Forwarded-For can otherwise
pick its own rate-limit bucket and bypass every limit, including the one in
front of login.
For the common Cloudflare → nginx → redrx chain:
TRUSTED_PROXIES=172.18.0.0/16 # nginx, as redrx sees it
USE_CLOUDFLARE=true
USE_CLOUDFLARE matters more than it looks. nginx appends the address it
received from to X-Forwarded-For, so the header arrives as
<visitor>, <cloudflare-edge> — the last entry is a Cloudflare server, not
your user. CF-Connecting-IP names the real visitor, so redrx prefers it. The
alternative is to add Cloudflare's published ranges to TRUSTED_PROXIES, which
lets redrx walk back past them; without one of the two, every visitor on the
internet shares the handful of buckets belonging to the edge servers.
Rate limits use the same syntax as before ("200 per day;50 per hour", "10 per minute", "5/hour"). RATELIMIT_STORAGE_URL accepts memory:// or a redis:// URL; when Redis is configured it also backs the GeoIP lookup cache. If Redis is unreachable at boot the service logs a warning and falls back to in-memory limiting rather than refusing to start, and if it becomes unreachable later each request is counted in memory on that replica, so the limits stay in force (per replica) instead of switching off.
For the full detailed documentation including parameter tables and system metrics, visit the /api-docs page on your running Redrx instance.
Include your personal API key in request headers. Generate it from the dashboard: only a hash is stored, so the key is shown once, when it is generated. Keys created before this change keep working.
X-API-KEY: your_api_key_here
POST /api/v1/shorten
Payload:
{
"long_url": "https://example.com/my-long-link",
"custom_code": "my-code",
"code_length": 6,
"preview_mode": true,
"stats_enabled": true,
"rotate_targets": ["https://alt1.com", "https://alt2.com"],
"ios_target_url": "https://apps.apple.com/app/id123",
"android_target_url": "https://play.google.com/store/apps/details?id=com.example",
"password": "secret-password",
"expiry_hours": 24,
"start_at": "2026-06-05T22:00:00Z",
"end_at": "2026-06-30T23:59:59Z"
}
Response (201 Created):
{
"short_code": "my-code",
"short_url": "https://short.example.com/my-code",
"long_url": "https://example.com/my-long-link",
"rotate_targets": ["https://alt1.com", "https://alt2.com"],
"ios_target_url": "https://apps.apple.com/app/id123",
"android_target_url": "https://play.google.com/store/apps/details?id=com.example",
"expires_at": "2026-06-06T22:00:00+00:00",
"start_at": "2026-06-05T22:00:00+00:00",
"end_at": "2026-06-30T23:59:59+00:00",
"password_protected": true,
"preview_mode": true,
"stats_enabled": true
}
GET /api/v1/<short_code>
Response (200 OK):
{
"short_code": "my-code",
"short_url": "https://short.example.com/my-code",
"long_url": "https://example.com/my-long-link",
"rotate_targets": ["https://alt1.com", "https://alt2.com"],
"ios_target_url": "https://apps.apple.com/app/id123",
"android_target_url": "https://play.google.com/store/apps/details?id=com.example",
"preview_mode": true,
"stats_enabled": true,
"clicks_count": 42,
"clicks": 42,
"created_at": "2026-06-05T22:00:00+00:00",
"expires_at": "2026-06-06T22:00:00+00:00",
"start_at": "2026-06-05T22:00:00+00:00",
"end_at": "2026-06-30T23:59:59+00:00",
"active": true
}
gosec, govulncheck and CodeQL run on every push and nightly.go.sum pins a checksum for every dependency; CI runs go mod verify, and Trivy and Gitleaks scan the tree.HttpOnly, SameSite=Lax, and Secure outside debug. CSRF tokens are per-session, constant-time compared, and rotated on login.ANONYMIZE_LOGS masks them in log output.This project is licensed under the MIT License. See LICENSE for details.
RATELIMIT_LOGIN| Access | DISABLE_ANONYMOUS_CREATE | false | When true, only authenticated users can shorten links. |
| Access | DISABLE_REGISTRATION | false | When true, public registration routes are disabled. |
| Abuse prevention | ANONYMOUS_POW_DIFFICULTY | 16 | Proof-of-work difficulty for anonymous link creation; 0 disables it, maximum 28. |
| Privacy | ENABLE_CONSENT_BANNER | false | Ask visitors for consent before recording anonymous click analytics. |
| Privacy | HONOR_DO_NOT_TRACK | true | Skip click analytics whenever the browser sends DNT: 1. |
| Proxy | TRUSTED_PROXIES | - | Peers whose X-Forwarded-* / CF-* headers are believed (IPs or CIDRs, comma separated; * for any). Empty means the headers are ignored. |
| Proxy | USE_CLOUDFLARE | false | Trust CF-Connecting-IP from a trusted proxy. Required for correct client IPs behind Cloudflare. |
| Server | LISTEN_ADDR | :5000 | Address the HTTP server binds to. |
| Server | METRICS_TOKEN | - | When set, /metrics requires Authorization: Bearer <token>. Unset leaves it public. |
| Server | REDRX_DEBUG | false | Development mode. Relaxes the canonical-domain redirect and allows a fallback SECRET_KEY. |