Self-Hosting Guide
Deploy LinkForty Core on your own infrastructure with complete control over your data.
This guide covers deploying LinkForty Core - the open-source deeplink engine. Core provides a REST API for link management, device-specific redirects, click analytics, deferred deep linking, QR codes, and webhooks. It does not include the Cloud dashboard, team management, or billing features. For a managed experience with a UI, see the Quick Start for LinkForty Cloud.
What You Get
LinkForty Core is a Fastify server backed by PostgreSQL (with optional Redis caching) that provides:
- REST API for creating, reading, updating, and deleting links
- Smart redirect routing with device detection (iOS, Android, web)
- Click analytics with geolocation, device type, and UTM tracking
- Deferred deep linking and fingerprint-based attribution
- QR code generation (PNG and SVG)
- Webhook notifications for real-time events
- Social previews via Open Graph tags
- iOS Universal Links and Android App Links support (
.well-knownendpoints)
All interactions with Core happen via its API. There is no built-in UI - you integrate it into your own application or use it as a standalone API service.
Why Self-Host?
Control and Flexibility:
- No per-install fees - unlimited scaling
- No vendor lock-in
- Deploy anywhere (AWS, GCP, Azure, DigitalOcean, on-premise)
Data Privacy:
- Complete data ownership
- GDPR/CCPA compliance on your terms
- No third-party data sharing
Customization:
- Full source code access (AGPL-3.0 license)
- Custom features and integrations
- Modify attribution logic for your use case
System Requirements
Minimum Specifications
Server:
- CPU: 2 cores
- RAM: 4 GB
- Storage: 20 GB SSD
- OS: Ubuntu 20.04+ / Debian 11+ / RHEL 8+
Software:
- Docker 20.10+ and Docker Compose 2.0+
- OR Node.js 18+ with PostgreSQL 14+ and Redis 7+ (optional)
Expected Load Capacity:
- ~1,000 requests/minute
- ~100,000 links
- ~1M clicks/month
Recommended Specifications (Production)
Server:
- CPU: 4 cores
- RAM: 8 GB
- Storage: 100 GB SSD
- OS: Ubuntu 22.04 LTS
Expected Load Capacity:
- ~10,000 requests/minute
- ~1M links
- ~10M clicks/month
Cloud Provider Options
Recommended instance types for minimum specifications:
| Provider | Instance Type | Specs |
|---|---|---|
| DigitalOcean | Droplet (Basic) | 2 CPU, 4GB RAM |
| AWS | t3.medium | 2 CPU, 4GB RAM |
| Google Cloud | e2-medium | 2 CPU, 4GB RAM |
| Hetzner | CX21 | 2 CPU, 4GB RAM |
| Vultr | Regular Performance | 2 CPU, 4GB RAM |
Installation Methods
Choose your deployment method:
- Method 1: Docker Compose (Recommended) - Fastest and easiest. Everything configured out of the box.
- Method 2: npm Package - Install Core as an npm dependency in your own Node.js application.
- Method 3: Manual Installation - For custom setups or when Docker isn't available.
- Method 4: Kubernetes - For enterprise deployments with high availability.
Method 1: Docker Compose (Recommended)
Step 1: Download Docker Compose File
mkdir linkforty && cd linkforty
curl -O https://raw.githubusercontent.com/linkforty/core/main/docker-compose.yml
Step 2: Configure Environment
Create a .env file:
# Database
POSTGRES_DB=linkforty
POSTGRES_USER=linkforty
POSTGRES_PASSWORD=your-strong-password
POSTGRES_PORT=5432
# Redis (optional but recommended for caching)
REDIS_URL=redis://redis:6379
REDIS_PORT=6379
# Server
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
LINKFORTY_PORT=3000
# CORS - set to your frontend domain
CORS_ORIGIN=*
# Behind a reverse proxy (nginx, Caddy, a load balancer): read the visitor's
# IP from X-Forwarded-For. Without this, every click and install is recorded
# with the proxy's address and deferred deep links are never attributed.
# Set it once you add the proxy in Step 6. Leave unset when clients reach
# Core directly.
# TRUST_PROXY=1
# Behind a CDN that sets its own client-IP header (e.g. Cloudflare): read
# that header instead. Only when the origin is reachable ONLY through the CDN.
# TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip
# Optional: base URL encoded in QR codes, including the scheme
# SHORTLINK_DOMAIN=https://yourdomain.com
# Optional: iOS Universal Links
# IOS_TEAM_ID=ABC123XYZ
# IOS_BUNDLE_ID=com.yourcompany.yourapp
# Optional: Android App Links
# ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
# ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
Security Note: Generate strong secrets with:
openssl rand -hex 32
Step 3: Start Services
docker compose up -d
This starts three containers:
- linkforty: The Core Fastify API server (port 3000)
- postgres: PostgreSQL database (port 5432)
- redis: Redis cache (port 6379)
The database schema is automatically initialized on first startup.
Step 4: Verify Installation
# Check all services are running
docker compose ps
# Test the API
curl http://localhost:3000/health
You should see a healthy response. The API is now available at http://localhost:3000.
Step 5: Create Your First Link
curl -X POST http://localhost:3000/api/links \
-H "Content-Type: application/json" \
-d '{
"userId": "user-1",
"originalUrl": "https://example.com",
"title": "My First Link",
"iosUrl": "https://apps.apple.com/app/your-app/id123456789",
"androidUrl": "https://play.google.com/store/apps/details?id=com.yourapp",
"webFallbackUrl": "https://example.com",
"utmParameters": {
"source": "twitter",
"medium": "social",
"campaign": "launch"
}
}'
The response will include a short_code you can use for redirects: http://localhost:3000/{shortCode}
Step 6: Set Up SSL (Required for Production)
Option A: Using Let's Encrypt (Free)
Install Certbot and Nginx:
sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx nginx
Configure Nginx as a reverse proxy:
# /etc/nginx/sites-available/linkforty
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# API and redirect routes
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
}
Enable site and generate certificate:
sudo ln -s /etc/nginx/sites-available/linkforty /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com
sudo systemctl reload nginx
Nginx now terminates every connection, so Core sees 127.0.0.1 as the client on every request. Core refuses to match an install to a click on a loopback or private address, so deferred deep linking does not work until Core reads the visitor's IP from the X-Forwarded-For header set above. Geo-targeting and the location columns in analytics depend on it too. Add this to .env and restart Core:
TRUST_PROXY=1
Option B: Using Cloudflare (Free SSL + CDN)
- Point your domain to Cloudflare nameservers
- Enable Full (strict) SSL mode in Cloudflare dashboard
- Create an origin certificate in Cloudflare
- Install the origin certificate on your server
- Enable Always Use HTTPS in Cloudflare
- Set
TRUSTED_CLIENT_IP_HEADER=cf-connecting-ipin.envand restart Core, so clicks and installs are recorded with the visitor's IP rather than a Cloudflare edge address. Restrict the origin to Cloudflare's IP ranges first, since the header can be forged by anyone who can reach the origin directly. See Preserve the client IP behind a proxy or CDN.
Method 2: npm Package
Install LinkForty Core as a dependency in your own Node.js application:
npm install @linkforty/core
Basic Server
import { createServer } from '@linkforty/core';
async function start() {
const server = await createServer({
database: {
url: 'postgresql://linkforty:password@localhost:5432/linkforty',
},
redis: {
url: 'redis://localhost:6379',
},
// Behind a reverse proxy or load balancer: the number of proxies in
// front of Core, so the visitor's IP is read from X-Forwarded-For.
// Omit it when clients connect to Core directly.
trustProxy: 1,
});
await server.listen({ port: 3000, host: '0.0.0.0' });
console.log('LinkForty Core running on http://localhost:3000');
}
start();
Using Individual Route Handlers
You can also register only specific routes in your own Fastify application:
import Fastify from 'fastify';
import { initializeDatabase, redirectRoutes, linkRoutes, analyticsRoutes } from '@linkforty/core';
const fastify = Fastify();
// Initialize database separately
await initializeDatabase({ url: 'postgresql://localhost/linkforty' });
// Register only the routes you need
await fastify.register(redirectRoutes);
await fastify.register(linkRoutes);
await fastify.register(analyticsRoutes);
await fastify.listen({ port: 3000 });
This gives you maximum flexibility to integrate LinkForty into an existing application.
Method 3: Manual Installation
Step 1: Install Dependencies
# Update system
sudo apt-get update && sudo apt-get upgrade -y
# Install Node.js 18+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install PostgreSQL 14+
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y postgresql-14
# Install Redis 7+ (optional but recommended)
sudo apt-get install -y redis-server
Step 2: Configure PostgreSQL
sudo -u postgres psql << EOF
CREATE DATABASE linkforty;
CREATE USER linkforty WITH PASSWORD 'your-strong-password';
GRANT ALL PRIVILEGES ON DATABASE linkforty TO linkforty;
\q
EOF
Step 3: Clone and Build
git clone https://github.com/linkforty/core.git
cd core
npm install
npm run build
Step 4: Configure Environment
cp .env.example .env
nano .env
Update the environment variables:
DATABASE_URL=postgresql://linkforty:your-strong-password@localhost:5432/linkforty
REDIS_URL=redis://localhost:6379
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
CORS_ORIGIN=*
# Set once Core sits behind nginx or another reverse proxy (Step 7), so the
# visitor's IP is read from X-Forwarded-For. Required for deferred deep linking.
# TRUST_PROXY=1
Step 5: Run Database Migrations
npm run migrate
Step 6: Set Up systemd Service
Create a service file for automatic startup:
sudo nano /etc/systemd/system/linkforty.service
[Unit]
Description=LinkForty Core
After=network.target postgresql.service redis.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/linkforty/core
Environment="NODE_ENV=production"
EnvironmentFile=/opt/linkforty/core/.env
ExecStart=/usr/bin/npx tsx examples/basic-server.ts
Restart=on-failure
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable linkforty
sudo systemctl start linkforty
# Check status
sudo systemctl status linkforty
Step 7: Put Nginx in Front
Follow Step 6 of the Docker Compose method for the nginx and certificate setup. Once nginx is in front of Core, set TRUST_PROXY=1 in .env and restart the service, or deferred deep linking will not attribute installs.
Method 4: Kubernetes
Prerequisites
- Kubernetes cluster (EKS, GKE, AKS, or self-hosted)
- kubectl configured
- Helm 3+
Step 1: Add Helm Repository
helm repo add linkforty https://charts.linkforty.com
helm repo update
Step 2: Create Values File
# values.yaml
replicaCount: 3
image:
repository: linkforty/core
tag: "latest"
pullPolicy: IfNotPresent
service:
type: LoadBalancer
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: linkforty.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: linkforty-tls
hosts:
- linkforty.yourdomain.com
postgresql:
enabled: true
auth:
username: linkforty
password: your-strong-password
database: linkforty
primary:
persistence:
size: 20Gi
redis:
enabled: true
auth:
enabled: false
master:
persistence:
size: 8Gi
env:
NODE_ENV: production
CORS_ORIGIN: "*"
# The ingress controller is a reverse proxy: read the visitor's IP from
# X-Forwarded-For. Use the number of proxies between the visitor and the pod.
TRUST_PROXY: "1"
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
Step 3: Install with Helm
helm install linkforty linkforty/core -f values.yaml
Step 4: Verify Deployment
kubectl get pods
kubectl get services
kubectl logs -l app=linkforty
Post-Installation Setup
1. Configure Custom Domain
Update DNS records to point to your server:
A yourdomain.com → your-server-ip
Then set the SHORTLINK_DOMAIN environment variable, so QR codes use that domain:
SHORTLINK_DOMAIN=https://yourdomain.com
2. Configure Mobile App Links
For iOS Universal Links and Android App Links, set the appropriate environment variables:
# iOS Universal Links
IOS_TEAM_ID=ABC123XYZ
IOS_BUNDLE_ID=com.yourcompany.yourapp
# Android App Links
ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
Core will automatically serve the .well-known/apple-app-site-association and .well-known/assetlinks.json files. See iOS Universal Links and Android App Links for the full setup.
3. Preserve the Client IP Behind a Proxy or CDN
Any reverse proxy in front of Core (nginx, Caddy, a load balancer, a Kubernetes ingress, Cloudflare) terminates the visitor's connection, so the IP Core sees on the socket is the proxy's, not the visitor's. Core matches a deferred install to the click that led to it by comparing device signals, and the IP is the one signal it requires. It also refuses to match on loopback and private addresses, because those cannot identify a device. So behind an unconfigured proxy, every install is recorded as organic and no deferred deep link is ever delivered. Geo-targeting and the country and city columns in analytics fail the same way.
Tell Core where the visitor's IP really is. Pick one:
# A reverse proxy that sets X-Forwarded-For (nginx, Caddy, HAProxy, a load
# balancer, an ingress controller). The value is the number of proxies
# between the visitor and Core; 1 for a single nginx.
TRUST_PROXY=1
# A CDN that sets its own authoritative client-IP header. Core reads that
# header and ignores X-Forwarded-For.
TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip # Cloudflare
TRUSTED_CLIENT_IP_HEADER=true-client-ip # Akamai and some other CDNs
Both settings trust what the proxy says, so Core must be reachable only through that proxy. Bind Core to 127.0.0.1 or firewall its port so a visitor cannot connect directly and forge the header. Leave both unset when clients connect to Core directly.
To verify, click a test link from a phone and check the click in link analytics: it should carry the visitor's country and city. If every click shows the server's own location or none, Core is still recording the proxy's address.
See the Environment Variables reference for details.
4. Set Up Backups
PostgreSQL Backup Script
#!/bin/bash
# /opt/linkforty/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/linkforty"
mkdir -p $BACKUP_DIR
# Backup database
docker compose exec -T postgres pg_dump -U linkforty linkforty | gzip > "$BACKUP_DIR/linkforty_$DATE.sql.gz"
# Keep only last 30 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete
# Upload to S3 (optional)
# aws s3 cp "$BACKUP_DIR/linkforty_$DATE.sql.gz" s3://your-backup-bucket/
Add to crontab:
crontab -e
# Daily backup at 2 AM
0 2 * * * /opt/linkforty/backup.sh
5. Set Up Monitoring
Using Docker Stats
docker stats linkforty-linkforty-1 linkforty-postgres-1
Using Prometheus + Grafana
# Add to docker-compose.yml
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3002:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
6. Configure Firewall
# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow SSH
sudo ufw allow 22/tcp
# Enable firewall
sudo ufw enable
Updating LinkForty
Docker Compose
cd /opt/linkforty
# Pull latest image
docker compose pull
# Restart with new image
docker compose up -d
Manual Installation
cd /opt/linkforty/core
# Pull latest code
git pull origin main
# Update dependencies
npm install
# Rebuild
npm run build
# Run migrations (if needed)
npm run migrate
# Restart service
sudo systemctl restart linkforty
Troubleshooting
Server Won't Start
# Check logs (Docker)
docker compose logs linkforty
# Check logs (systemd)
sudo journalctl -u linkforty -f
# Common issues:
# 1. Database not ready - wait 10 seconds and retry
# 2. Redis not accessible - check REDIS_URL (Redis is optional, remove REDIS_URL to skip)
# 3. Port 3000 in use - change PORT in .env
Deferred Deep Links Never Arrive
Every install reports as organic, and the SDK's deferred deep link callback receives null, while direct deep links work. In order of likelihood:
- Core is behind a proxy and does not know it. Set
TRUST_PROXY=1(orTRUSTED_CLIENT_IP_HEADERbehind a CDN) and restart. See Preserve the client IP behind a proxy or CDN. This is the cause whenever the server sits behind nginx or Cloudflare and the variable was never set. - Android, but the SDK is too old to send the Play install referrer. Core attaches the click id to every Google Play link it redirects to, and an SDK that reads the Play install referrer matches the install to that exact click. That needs Core 1.41.1 or later and an SDK that sends the referrer: Android SDK 1.8.0, React Native 1.9.0, Expo 1.9.0, or Flutter 0.6.0 and later.
- The click fell outside the attribution window. The default is 7 days. See Attribution windows.
The server log line for the install request shows the confidence score and the factors that matched, which tells you where the match fails.
Database Connection Errors
# Test PostgreSQL connection (Docker)
docker compose exec postgres psql -U linkforty -d linkforty -c "SELECT 1;"
# Check DATABASE_URL format:
# postgresql://username:password@host:port/database
High Memory Usage
# Increase PostgreSQL shared_buffers
docker compose exec postgres psql -U linkforty -d linkforty -c "ALTER SYSTEM SET shared_buffers = '256MB';"
docker compose restart postgres
# Increase Node.js memory limit
# Add to environment in docker-compose.yml:
# NODE_OPTIONS: "--max-old-space-size=2048"
Redis Connection Errors
Redis is optional. If you don't need caching, remove the REDIS_URL environment variable and Core will fall back to direct database queries. If you do use Redis and see connection errors:
# Check Redis is running
docker compose exec redis redis-cli ping
# Should return: PONG
Production Checklist
Before going live:
- SSL certificate installed and working
TRUST_PROXY(orTRUSTED_CLIENT_IP_HEADERbehind a CDN) set, so clicks and installs are recorded with the visitor's IP- Strong database password set (not the default
changeme) - Database backups configured (daily minimum)
- Firewall rules configured
- Monitoring set up
- DNS records pointing to server
- Rate limiting configured (if needed)
- Test link created and redirect working
- Health check endpoint responding (
/health) - CORS configured for your frontend domain
API Reference
Once Core is running, all interactions happen through the REST API:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/links | Create a new link |
| GET | /api/links?userId=... | List all links for a user |
| GET | /api/links/:id?userId=... | Get a specific link |
| PUT | /api/links/:id?userId=... | Update a link |
| DELETE | /api/links/:id?userId=... | Delete a link |
| GET | /api/analytics/overview?userId=... | Analytics overview |
| GET | /api/analytics/links/:id?userId=... | Link-specific analytics |
| GET | /:shortCode | Redirect (public, no auth) |
For full API documentation, see the API Reference.
Getting Help
- API Reference - Complete API documentation
- GitHub Repository - Source code and releases
- Report Issues - Bug reports
- GitHub Discussions - Community support
Next Steps
- Environment Variables - Full configuration reference
- API Authentication - Learn how to authenticate API requests
- Mobile SDK Integration - Set up deep linking in your app
- Webhook Configuration - Real-time event notifications
- Cloud vs Self-Hosted - Compare with the managed Cloud option