Skip to content

Repository files navigation

LinkForty Logo

LinkForty Core

Open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links

Self-hosted deep linking engine with device detection, analytics, deferred deep linking, and smart routing. No per-click pricing, no vendor lock-in, full data ownership — runs on your own PostgreSQL. Firebase Dynamic Links shut down in August 2025; LinkForty is a production-ready, open-source replacement you can deploy today.

npm versionCIcodecovDocker PullsDocker Image SizeLicense: AGPL v3

Why LinkForty?

  • Self-hosted and open-source — AGPL-3.0 licensed, deploy on your own infrastructure
  • No per-click pricing — No usage-based fees, no monthly minimums, no enterprise sales calls
  • Full data ownership — All click data, analytics, and attribution stored in your PostgreSQL database
  • Privacy-first — No third-party data sharing, no tracking pixels, your users' data stays with you
  • Drop-in replacement — REST API + mobile SDKs for React Native, Expo, iOS (Swift), and Android (Kotlin)
  • Firebase Dynamic Links replacement — Google shut down Firebase Dynamic Links in August 2025. LinkForty provides the same capabilities with a self-hosted, open-source stack

How LinkForty Compares

FeatureLinkForty CoreBranchAppsFlyerFirebase Dynamic Links
Open SourceYes (AGPL-3.0)NoNoNo
Self-HostedYesNoNoNo
Data OwnershipCompleteVendor-controlledVendor-controlledWas Google-controlled
Deferred Deep LinkingYesYesYesWas supported
Device Detection & RoutingYesYesYesWas supported
Click AnalyticsYesYesYesBasic
QR Code GenerationBuilt-inNoNoNo
WebhooksYesEnterprise onlyEnterprise onlyNo
iOS Universal LinksYesYesYesWas supported
Android App LinksYesYesYesWas supported
UTM Parameter TrackingYesYesCustom paramsWas supported
Custom DomainsYesEnterprise onlyEnterprise onlyNo

Features

  • Smart Link Routing - Create short links with device-specific URLs for iOS, Android, and web
  • Device Detection - Automatic detection and routing based on user device
  • Click Analytics - Track clicks with geolocation, device type, platform, and more
  • UTM Parameters - Built-in support for UTM campaign tracking
  • Targeting Rules - Filter by country, device, and language before redirecting
  • QR Code Generation - Generate QR codes (PNG/SVG) for any link
  • Deferred Deep Linking - Probabilistic fingerprint matching for install attribution
  • Webhooks - Event-driven integrations with HMAC-signed payloads and retry logic
  • Smart App Opening - Mobile clicks serve an interstitial that tries the app via URI scheme, falls back to the App Store / Play Store. Preserves URL fragments for E2E encryption keys
  • OG Preview Pages - Social media scraper detection with Open Graph meta tags
  • iOS Universal Links & Android App Links - Serve .well-known files automatically
  • Link Expiration - Set expiration dates for time-sensitive links
  • Redis Caching - Optional Redis support for high-performance link lookups
  • PostgreSQL Storage - Reliable data persistence with full SQL capabilities
  • TypeScript - Fully typed API for better developer experience
  • No Auth Included - Bring your own authentication; userId is optional for multi-tenant scoping

Installation

npm install @linkforty/core

Quick Start

1. Basic Server

import{createServer}from'@linkforty/core';asyncfunctionstart(){constserver=awaitcreateServer({database: {url: 'postgresql://localhost/linkforty',},redis: {url: 'redis://localhost:6379',},});awaitserver.listen({port: 3000,host: '0.0.0.0'});console.log('Server running on http://localhost:3000');}start();

2. Docker (Recommended for Production)

Quick Start:

# Pull the latest image
docker pull linkforty/core:latest
# Run with Docker Compose
curl -O https://raw.githubusercontent.com/linkforty/core/main/docker-compose.yml
docker compose up -d

Or use Docker CLI:

docker run -d \
--name linkforty \
-p 3000:3000 \
-e DATABASE_URL=postgresql://user:pass@host:5432/linkforty?sslmode=disable \
-e REDIS_URL=redis://host:6379 \
linkforty/core:latest

Features:

  • Pre-built multi-architecture images (AMD64 + ARM64)
  • Automatic updates with version tags
  • Non-root user for security
  • Built-in health checks
  • Supply chain attestations (SBOM + Provenance)

See DOCKER.md for complete deployment guide.

API Reference

Links

Create a Link

userId is optional. When provided, the link is scoped to that user (multi-tenant mode). When omitted, the link has no owner (single-tenant mode).

POST /api/links
Content-Type: application/json
{
"userId": "user-uuid",
"originalUrl": "https://example.com",
"title": "My Link",
"description": "Summer campaign link",
"iosAppStoreUrl": "https://apps.apple.com/app/id123456",
"androidAppStoreUrl": "https://play.google.com/store/apps/details?id=com.example",
"webFallbackUrl": "https://example.com/product/123",
"appScheme": "myapp",
"iosUniversalLink": "https://example.com/app/product/123",
"androidAppLink": "https://example.com/app/product/123",
"deepLinkPath": "/product/123",
"deepLinkParameters": { "ref": "campaign-1" },
"utmParameters": {
"source": "twitter",
"medium": "social",
"campaign": "summer-sale"
},
"ogTitle": "Check out this deal",
"ogDescription": "50% off summer sale",
"ogImageUrl": "https://example.com/og-image.png",
"targetingRules": {
"countries": ["US", "CA"],
"devices": ["ios", "android"],
"languages": ["en"]
},
"attributionWindowHours": 168,
"customCode": "summer-sale",
"expiresAt": "2026-12-31T23:59:59Z"
}

All fields except originalUrl are optional.

Get All Links

# Single-tenant (all links)
GET /api/links
# Multi-tenant (scoped to user)
GET /api/links?userId=user-uuid

Get a Specific Link

GET /api/links/:id
GET /api/links/:id?userId=user-uuid

Update a Link

PUT /api/links/:id?userId=user-uuid
Content-Type: application/json
{
"title": "Updated Title",
"isActive": false
}

Duplicate a Link

POST /api/links/:id/duplicate?userId=user-uuid

Delete a Link

DELETE /api/links/:id?userId=user-uuid

Analytics

Get Analytics Overview

# All links
GET /api/analytics/overview?days=30
# Scoped to user
GET /api/analytics/overview?userId=user-uuid&days=30

Returns: totalClicks, uniqueClicks, clicksByDate, clicksByCountry, clicksByDevice, clicksByPlatform, topLinks

Get Link-Specific Analytics

GET /api/analytics/links/:linkId?days=30

Redirect

GET /:shortCode
GET /:templateSlug/:shortCode

Automatically redirects users to the appropriate URL based on device type (iOS/Android/web), evaluates targeting rules, and tracks the click asynchronously.

Mobile interstitial: When a link has appScheme configured and a store fallback URL (iOS App Store or Google Play), mobile requests receive a smart interstitial page instead of a raw 302 redirect. The interstitial tries to open the app via URI scheme and falls back to the app store after 1.5 seconds. This handles the case where a 302 to a custom URI scheme fails silently when the app is not installed. URL fragments are preserved through the redirect, enabling patterns like E2E encryption where the decryption key lives in the fragment.

QR Codes

GET /api/links/:id/qr?format=png&size=300
GET /api/links/:id/qr?format=svg

Webhooks

GET /api/webhooks?userId=user-uuid
POST /api/webhooks # Body: { name, url, events, userId? }
GET /api/webhooks/:id?userId=user-uuid
PUT /api/webhooks/:id?userId=user-uuid
DELETE /api/webhooks/:id?userId=user-uuid
POST /api/webhooks/:id/test?userId=user-uuid

Events: click_event, install_event, conversion_event, sdk_event. Payloads are HMAC SHA-256 signed.

Mobile SDK Endpoints

POST /api/sdk/v1/install # Report app install, get deferred deep link
GET /api/sdk/v1/attribution/:fingerprint # Debug attribution lookups
POST /api/sdk/v1/event # Track in-app conversion events
GET /api/sdk/v1/resolve/:shortCode # Resolve link to deep link data (no redirect)
GET /api/sdk/v1/health # Health check

Health

GET /health # Liveness — process is up (no DB access)
GET /health/ready # Readiness — 503 if the database is unreachable

Debug & Testing

POST /api/debug/simulate # Simulate a link click with custom parameters
WS /api/debug/live?userId=user-uuid # WebSocket live click event stream
GET /api/debug/user-agents # Common UA strings for testing
GET /api/debug/countries # Common countries list
GET /api/debug/languages # Common languages list

Well-Known Routes

GET /.well-known/apple-app-site-association # iOS Universal Links
GET /.well-known/assetlinks.json # Android App Links

OG Preview

GET /:shortCode/preview # OG meta tag page for social scrapers

Configuration

Server Options

interfaceServerOptions{database?: {url?: string;// PostgreSQL connection stringpool?: {min?: number;// Minimum pool connections (default: 2)max?: number;// Maximum pool connections (default: 10)};};redis?: {url: string;// Redis connection string (optional)};cors?: {origin: string|string[];// CORS allowed origins (default: '*')};logger?: boolean;// Enable Fastify logger (default: true)trustProxy?: boolean|number;// Trust X-Forwarded-For when behind a proxy (default: false)}

Running behind a reverse proxy

When Core runs behind a reverse proxy, CDN, or load balancer, set trustProxy so the server uses the real client IP from X-Forwarded-For for redirect targeting, geo, attribution, and fingerprinting. Pass it when creating the server (e.g. trustProxy: true or a number of proxy hops) or set the TRUST_PROXY environment variable (e.g. TRUST_PROXY=1). Client-provided ipAddress in the SDK install request body is not used as the trusted IP; it is optional debug metadata only and must not be relied on for attribution.

Environment Variables

DATABASE_URL=postgresql://localhost/linkforty
REDIS_URL=redis://localhost:6379
PORT=3000
NODE_ENV=production
CORS_ORIGIN=*# When behind a reverse proxy: TRUST_PROXY=1 (or number of hops) so client IP is read from X-Forwarded-For# TRUST_PROXY=1# Mobile SDK (optional — for iOS Universal Links and Android App Links)
IOS_TEAM_ID=ABC123XYZ
IOS_BUNDLE_ID=com.yourcompany.yourapp
ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
# Custom domain for QR code URLs (optional)
SHORTLINK_DOMAIN=yourdomain.com

Database Schema

Core does not create a users table. Authentication and user management are the consumer's responsibility. The user_id column on links and webhooks is optional (nullable, no foreign key) — use it for multi-tenant scoping when your auth layer provides a user identity.

Links Table

ColumnTypeDescription
idUUIDPrimary key
user_idUUIDOptional owner/tenant identifier
short_codeVARCHAR(20)Unique short code
original_urlTEXTOriginal URL
titleVARCHAR(255)Link title
descriptionTEXTLink description
ios_app_store_urlTEXTiOS App Store URL
android_app_store_urlTEXTAndroid Play Store URL
web_fallback_urlTEXTWeb fallback URL
app_schemeVARCHAR(255)URI scheme (e.g., "myapp")
ios_universal_linkTEXTiOS Universal Link URL
android_app_linkTEXTAndroid App Link URL
deep_link_pathTEXTIn-app destination path
deep_link_parametersJSONBCustom app parameters
utm_parametersJSONBUTM tracking parameters
targeting_rulesJSONBCountry/device/language targeting
og_titleVARCHAR(255)Open Graph title
og_descriptionTEXTOpen Graph description
og_image_urlTEXTOpen Graph image URL
og_typeVARCHAR(50)Open Graph type (default: "website")
attribution_window_hoursINTEGERInstall attribution window (default: 168)
is_activeBOOLEANActive status
expires_atTIMESTAMPExpiration date
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp

Click Events Table

ColumnTypeDescription
idUUIDPrimary key
link_idUUIDForeign key to links
clicked_atTIMESTAMPClick timestamp
ip_addressINETUser IP address
user_agentTEXTUser agent string
device_typeVARCHAR(20)Device type (ios/android/web)
platformVARCHAR(20)Platform (iOS/Android/Web)
country_codeCHAR(2)Country code
country_nameVARCHAR(100)Country name
regionVARCHAR(100)Region/state
cityVARCHAR(100)City
latitudeDECIMALLatitude
longitudeDECIMALLongitude
timezoneVARCHAR(100)Timezone
utm_sourceVARCHAR(255)UTM source
utm_mediumVARCHAR(255)UTM medium
utm_campaignVARCHAR(255)UTM campaign
referrerTEXTReferrer URL

Device Fingerprints Table

ColumnTypeDescription
idUUIDPrimary key
click_idUUIDForeign key to click_events
fingerprint_hashVARCHAR(64)SHA-256 hash of fingerprint signals
ip_addressINETIP address
user_agentTEXTUser agent string
timezoneVARCHAR(100)Timezone
languageVARCHAR(10)Browser language
screen_widthINTEGERScreen width
screen_heightINTEGERScreen height
platformVARCHAR(50)Platform
platform_versionVARCHAR(50)Platform version
created_atTIMESTAMPCreation timestamp

Install Events Table

ColumnTypeDescription
idUUIDPrimary key
link_idUUIDAttributed link (nullable)
click_idUUIDAttributed click (nullable)
fingerprint_hashVARCHAR(64)Device fingerprint hash
confidence_scoreDECIMALMatch confidence (0-100)
installed_atTIMESTAMPInstall timestamp
first_open_atTIMESTAMPFirst app open
deep_link_retrievedBOOLEANWhether deferred link was fetched
deep_link_dataJSONBDeferred deep link data
attribution_window_hoursINTEGERAttribution window used (default: 168)
device_idVARCHAR(255)Optional device identifier
created_atTIMESTAMPCreation timestamp

In-App Events Table

ColumnTypeDescription
idUUIDPrimary key
install_idUUIDForeign key to install_events
event_nameVARCHAR(255)Event name
event_dataJSONBCustom event properties
event_timestampTIMESTAMPWhen the event occurred
created_atTIMESTAMPCreation timestamp

Webhooks Table

ColumnTypeDescription
idUUIDPrimary key
user_idUUIDOptional owner/tenant identifier
nameVARCHAR(255)Webhook name
urlTEXTDelivery URL
secretVARCHAR(255)HMAC signing secret
eventsTEXT[]Subscribed event types
is_activeBOOLEANActive status
retry_countINTEGERMax retries (default: 3)
timeout_msINTEGERRequest timeout (default: 10000)
headersJSONBCustom HTTP headers
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp

Utilities

Generate Short Code

import{generateShortCode}from'@linkforty/core';constcode=generateShortCode(8);// Returns 8-character nanoid

Detect Device

import{detectDevice}from'@linkforty/core';constdevice=detectDevice(userAgent);// Returns 'ios' | 'android' | 'web'

Get Location from IP

import{getLocationFromIP}from'@linkforty/core';constlocation=getLocationFromIP('8.8.8.8');// Returns: { countryCode, countryName, region, city, latitude, longitude, timezone }

Build Redirect URL with UTM Parameters

import{buildRedirectUrl}from'@linkforty/core';consturl=buildRedirectUrl('https://example.com',{source: 'twitter',medium: 'social',campaign: 'summer-sale'});// Returns: https://example.com?utm_source=twitter&utm_medium=social&utm_campaign=summer-sale

Advanced Usage

Custom Route Registration

import{createServer}from'@linkforty/core';constserver=awaitcreateServer({database: {url: 'postgresql://localhost/linkforty'},});// Add custom routesserver.get('/custom',async(request,reply)=>{return{message: 'Hello World'};});awaitserver.listen({port: 3000});

Using Individual Route Handlers

importFastifyfrom'fastify';import{initializeDatabase,redirectRoutes,linkRoutes}from'@linkforty/core';constfastify=Fastify();// Initialize database separatelyawaitinitializeDatabase({url: 'postgresql://localhost/linkforty'});// Register only specific routesawaitfastify.register(redirectRoutes);awaitfastify.register(linkRoutes);awaitfastify.listen({port: 3000});

Deployment

LinkForty can be deployed in multiple ways depending on your needs:

Production Deployment (Recommended)

Deploy to managed platforms with minimal DevOps overhead:

Fly.io (Recommended)

  • Global edge deployment
  • Managed PostgreSQL and Redis
  • Auto-scaling and SSL included
  • Starting at ~$10-15/month

View Fly.io deployment guide

See infra/ directory for all deployment options and platform-specific guides.

Docker Deployment (Recommended for Self-Hosting)

Production-ready Docker images available on Docker Hub:

# One-command deployment
curl -O https://raw.githubusercontent.com/linkforty/core/main/docker-compose.yml
docker compose up -d

Image Details:

  • Registry:linkforty/core
  • Tags:latest, v1.x.x, main
  • Architectures: linux/amd64, linux/arm64
  • Base: Node.js 22 Alpine (minimal, secure)
  • Security: Non-root user, SBOM attestations

Version Pinning (Recommended):

services:
linkforty:
image: linkforty/core:v1.5.0 # Pin to specific version

See DOCKER.md for complete deployment guide including:

  • Environment configuration
  • Health checks
  • Backup strategies
  • Production best practices

Manual Deployment

For custom infrastructure needs:

  1. Install dependencies: npm install @linkforty/core
  2. Set up PostgreSQL database (13+)
  3. Set up Redis (optional but recommended)
  4. Run migrations: npm run migrate
  5. Start server: node server.js

Other Platforms

Community-maintained templates available for:

  • AWS (ECS/Fargate)
  • Google Cloud Run
  • Railway, Render, and more

See infra/CONTRIBUTING.md to add support for additional platforms.

Performance

  • Redis caching: 5-minute TTL on link lookups reduces database queries by 90%
  • Database indexes: Optimized queries for fast link lookups and analytics
  • Async click tracking: Non-blocking click event logging via setImmediate()
  • Connection pooling: Efficient database connection management (min 2, max 10)

Security

  • SQL injection protection: Parameterized queries throughout
  • Input validation: Zod schema validation on all inputs
  • CORS configuration: Configurable CORS for API access control
  • Link expiration: Automatic handling of expired links
  • Webhook signing: HMAC SHA-256 signed payloads
  • No auth included: Core does not include authentication. The optional userId parameter provides data scoping but does not verify identity. Add your own auth middleware as needed.

Mobile SDK Integration

LinkForty Core supports iOS Universal Links and Android App Links for seamless deep linking in mobile applications.

iOS Universal Links Setup

  1. Set environment variables:

    IOS_TEAM_ID=ABC123XYZ # Your Apple Developer Team ID
    IOS_BUNDLE_ID=com.yourcompany.yourapp
  2. Configure in Xcode:

    • Add "Associated Domains" capability
    • Add domain: applinks:yourdomain.com
  3. Verify AASA file:

    curl https://yourdomain.com/.well-known/apple-app-site-association

    Expected response:

    {
    "applinks": {
    "apps": [],
    "details": [
    {
    "appID": "ABC123XYZ.com.yourcompany.yourapp",
    "paths": ["*"]
    }
    ]
    }
    }

Android App Links Setup

  1. Get your SHA-256 fingerprint:

    # Debug keystore
    keytool -list -v -keystore ~/.android/debug.keystore \
    -alias androiddebugkey -storepass android -keypass android
    # Release keystore
    keytool -list -v -keystore /path/to/release.keystore \
    -alias your-alias
  2. Set environment variables:

    ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
    ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
    # Multiple fingerprints (debug + release)
    ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:...,DD:EE:FF:...
  3. Configure in AndroidManifest.xml:

    <intent-filterandroid:autoVerify="true">
    <actionandroid:name="android.intent.action.VIEW" />
    <categoryandroid:name="android.intent.category.DEFAULT" />
    <categoryandroid:name="android.intent.category.BROWSABLE" />
    <dataandroid:scheme="https" />
    <dataandroid:host="yourdomain.com" />
    </intent-filter>
  4. Verify assetlinks.json:

    curl https://yourdomain.com/.well-known/assetlinks.json

    Expected response:

    [
    {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
    "namespace": "android_app",
    "package_name": "com.yourcompany.yourapp",
    "sha256_cert_fingerprints": ["AA:BB:CC:..."]
    }
    }
    ]

Available Mobile SDKs

PlatformPackageInstall
React Native@linkforty/mobile-sdk-react-nativenpm install @linkforty/mobile-sdk-react-native
Expo@linkforty/mobile-sdk-exponpx expo install @linkforty/mobile-sdk-expo
iOS (Swift)LinkFortySDKSwift Package Manager
Android (Kotlin)LinkFortySDKGradle dependency

See the SDK documentation for integration guides.

Testing Domain Verification

Test iOS Universal Links with Apple's validator:

https://search.developer.apple.com/appsearch-validation-tool/

Test Android App Links with Google's validator:

adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/test"

Migrate from Another Platform

Switching from an existing deep linking provider? LinkForty supports zero-downtime migration via custom domain DNS cutover.

For AI Tools (llms.txt)

LinkForty provides machine-readable documentation for AI coding assistants (Claude, ChatGPT, Cursor, Copilot).

Download into your project for AI-assisted integration:

curl -o LINKFORTY.md https://docs.linkforty.com/llms-full.txt

The npm package also ships with an llms.txt file — AI tools that read from node_modules can discover it automatically.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details.

License

AGPL-3.0 - see LICENSE file for details.

Related Projects

Support

Built with:

About

Open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Self-hosted deep linking engine with device detection, analytics, deferred deep linking, and smart routing.

Topics

Resources

Contributing

Security policy

Stars

34 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages