Skip to main content

Self-Hosting Guide

Deploy LinkForty Core on your own infrastructure with complete control over your data.

What is LinkForty Core?

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-known endpoints)

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

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:

ProviderInstance TypeSpecs
DigitalOceanDroplet (Basic)2 CPU, 4GB RAM
AWSt3.medium2 CPU, 4GB RAM
Google Cloude2-medium2 CPU, 4GB RAM
HetznerCX212 CPU, 4GB RAM
VultrRegular Performance2 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.

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.

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
Tell Core it is behind the proxy

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

See Preserve the client IP behind a proxy or CDN.

Option B: Using Cloudflare (Free SSL + CDN)​

  1. Point your domain to Cloudflare nameservers
  2. Enable Full (strict) SSL mode in Cloudflare dashboard
  3. Create an origin certificate in Cloudflare
  4. Install the origin certificate on your server
  5. Enable Always Use HTTPS in Cloudflare
  6. Set TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip in .env and 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

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

Every install reports as organic, and the SDK's deferred deep link callback receives null, while direct deep links work. In order of likelihood:

  1. Core is behind a proxy and does not know it. Set TRUST_PROXY=1 (or TRUSTED_CLIENT_IP_HEADER behind 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.
  2. 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.
  3. 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 (or TRUSTED_CLIENT_IP_HEADER behind 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:

MethodEndpointDescription
POST/api/linksCreate 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/:shortCodeRedirect (public, no auth)

For full API documentation, see the API Reference.


Getting Help​

Next Steps​