@linkforty/core
v1.6.6
Published
Open-source deeplink management engine with device detection and analytics
Maintainers
Readme
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.
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
| Feature | LinkForty Core | Branch | AppsFlyer | Firebase Dynamic Links | |---------|---------------|--------|-----------|----------------------| | Pricing | Free (self-hosted) | Starts at $299/mo | Starts at $500/mo | Shut down (Aug 2025) | | Open Source | Yes (AGPL-3.0) | No | No | No | | Self-Hosted | Yes | No | No | No | | Data Ownership | Complete | Vendor-controlled | Vendor-controlled | Was Google-controlled | | Deferred Deep Linking | Yes | Yes | Yes | Was supported | | Device Detection & Routing | Yes | Yes | Yes | Was supported | | Click Analytics | Yes | Yes | Yes | Basic | | QR Code Generation | Built-in | No | No | No | | Webhooks | Yes | Enterprise only | Enterprise only | No | | iOS Universal Links | Yes | Yes | Yes | Was supported | | Android App Links | Yes | Yes | Yes | Was supported | | UTM Parameter Tracking | Yes | Yes | Custom params | Was supported | | Custom Domains | Yes | Enterprise only | Enterprise only | No |
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
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/coreQuick Start
1. Basic Server
import { createServer } from '@linkforty/core';
async function start() {
const server = await createServer({
database: {
url: 'postgresql://localhost/linkforty',
},
redis: {
url: 'redis://localhost:6379',
},
});
await server.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 -dOr 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:latestFeatures:
- 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-uuidGet a Specific Link
GET /api/links/:id
GET /api/links/:id?userId=user-uuidUpdate 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-uuidDelete a Link
DELETE /api/links/:id?userId=user-uuidAnalytics
Get Analytics Overview
# All links
GET /api/analytics/overview?days=30
# Scoped to user
GET /api/analytics/overview?userId=user-uuid&days=30Returns: totalClicks, uniqueClicks, clicksByDate, clicksByCountry, clicksByDevice, clicksByPlatform, topLinks
Get Link-Specific Analytics
GET /api/analytics/links/:linkId?days=30Redirect
GET /:shortCode
GET /:templateSlug/:shortCodeAutomatically redirects users to the appropriate URL based on device type (iOS/Android/web), evaluates targeting rules, and tracks the click asynchronously.
QR Codes
GET /api/links/:id/qr?format=png&size=300
GET /api/links/:id/qr?format=svgWebhooks
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-uuidEvents: click_event, install_event, conversion_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 checkDebug & 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 listWell-Known Routes
GET /.well-known/apple-app-site-association # iOS Universal Links
GET /.well-known/assetlinks.json # Android App LinksOG Preview
GET /:shortCode/preview # OG meta tag page for social scrapersConfiguration
Server Options
interface ServerOptions {
database?: {
url?: string; // PostgreSQL connection string
pool?: {
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)
}Environment Variables
DATABASE_URL=postgresql://localhost/linkforty
REDIS_URL=redis://localhost:6379
PORT=3000
NODE_ENV=production
CORS_ORIGIN=*
# 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.comDatabase 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
| Column | Type | Description | |-------------------------|--------------|------------------------------------------| | id | UUID | Primary key | | user_id | UUID | Optional owner/tenant identifier | | short_code | VARCHAR(20) | Unique short code | | original_url | TEXT | Original URL | | title | VARCHAR(255) | Link title | | description | TEXT | Link description | | ios_app_store_url | TEXT | iOS App Store URL | | android_app_store_url | TEXT | Android Play Store URL | | web_fallback_url | TEXT | Web fallback URL | | app_scheme | VARCHAR(255) | URI scheme (e.g., "myapp") | | ios_universal_link | TEXT | iOS Universal Link URL | | android_app_link | TEXT | Android App Link URL | | deep_link_path | TEXT | In-app destination path | | deep_link_parameters | JSONB | Custom app parameters | | utm_parameters | JSONB | UTM tracking parameters | | targeting_rules | JSONB | Country/device/language targeting | | og_title | VARCHAR(255) | Open Graph title | | og_description | TEXT | Open Graph description | | og_image_url | TEXT | Open Graph image URL | | og_type | VARCHAR(50) | Open Graph type (default: "website") | | attribution_window_hours| INTEGER | Install attribution window (default: 168)| | is_active | BOOLEAN | Active status | | expires_at | TIMESTAMP | Expiration date | | created_at | TIMESTAMP | Creation timestamp | | updated_at | TIMESTAMP | Last update timestamp |
Click Events Table
| Column | Type | Description | |--------------|--------------|------------------------------| | id | UUID | Primary key | | link_id | UUID | Foreign key to links | | clicked_at | TIMESTAMP | Click timestamp | | ip_address | INET | User IP address | | user_agent | TEXT | User agent string | | device_type | VARCHAR(20) | Device type (ios/android/web)| | platform | VARCHAR(20) | Platform (iOS/Android/Web) | | country_code | CHAR(2) | Country code | | country_name | VARCHAR(100) | Country name | | region | VARCHAR(100) | Region/state | | city | VARCHAR(100) | City | | latitude | DECIMAL | Latitude | | longitude | DECIMAL | Longitude | | timezone | VARCHAR(100) | Timezone | | utm_source | VARCHAR(255) | UTM source | | utm_medium | VARCHAR(255) | UTM medium | | utm_campaign | VARCHAR(255) | UTM campaign | | referrer | TEXT | Referrer URL |
Device Fingerprints Table
| Column | Type | Description | |------------------|-------------|-------------------------------------| | id | UUID | Primary key | | click_id | UUID | Foreign key to click_events | | fingerprint_hash | VARCHAR(64) | SHA-256 hash of fingerprint signals | | ip_address | INET | IP address | | user_agent | TEXT | User agent string | | timezone | VARCHAR(100)| Timezone | | language | VARCHAR(10) | Browser language | | screen_width | INTEGER | Screen width | | screen_height | INTEGER | Screen height | | platform | VARCHAR(50) | Platform | | platform_version | VARCHAR(50) | Platform version | | created_at | TIMESTAMP | Creation timestamp |
Install Events Table
| Column | Type | Description | |-------------------------|-------------|------------------------------------| | id | UUID | Primary key | | link_id | UUID | Attributed link (nullable) | | click_id | UUID | Attributed click (nullable) | | fingerprint_hash | VARCHAR(64) | Device fingerprint hash | | confidence_score | DECIMAL | Match confidence (0-100) | | installed_at | TIMESTAMP | Install timestamp | | first_open_at | TIMESTAMP | First app open | | deep_link_retrieved | BOOLEAN | Whether deferred link was fetched | | deep_link_data | JSONB | Deferred deep link data | | attribution_window_hours| INTEGER | Attribution window used (default: 168) | | device_id | VARCHAR(255)| Optional device identifier | | created_at | TIMESTAMP | Creation timestamp |
In-App Events Table
| Column | Type | Description | |-----------------|--------------|------------------------------| | id | UUID | Primary key | | install_id | UUID | Foreign key to install_events| | event_name | VARCHAR(255) | Event name | | event_data | JSONB | Custom event properties | | event_timestamp | TIMESTAMP | When the event occurred | | created_at | TIMESTAMP | Creation timestamp |
Webhooks Table
| Column | Type | Description | |------------|--------------|----------------------------------| | id | UUID | Primary key | | user_id | UUID | Optional owner/tenant identifier | | name | VARCHAR(255) | Webhook name | | url | TEXT | Delivery URL | | secret | VARCHAR(255) | HMAC signing secret | | events | TEXT[] | Subscribed event types | | is_active | BOOLEAN | Active status | | retry_count| INTEGER | Max retries (default: 3) | | timeout_ms | INTEGER | Request timeout (default: 10000) | | headers | JSONB | Custom HTTP headers | | created_at | TIMESTAMP | Creation timestamp | | updated_at | TIMESTAMP | Last update timestamp |
Utilities
Generate Short Code
import { generateShortCode } from '@linkforty/core';
const code = generateShortCode(8); // Returns 8-character nanoidDetect Device
import { detectDevice } from '@linkforty/core';
const device = detectDevice(userAgent); // Returns 'ios' | 'android' | 'web'Get Location from IP
import { getLocationFromIP } from '@linkforty/core';
const location = getLocationFromIP('8.8.8.8');
// Returns: { countryCode, countryName, region, city, latitude, longitude, timezone }Build Redirect URL with UTM Parameters
import { buildRedirectUrl } from '@linkforty/core';
const url = buildRedirectUrl('https://example.com', {
source: 'twitter',
medium: 'social',
campaign: 'summer-sale'
});
// Returns: https://example.com?utm_source=twitter&utm_medium=social&utm_campaign=summer-saleAdvanced Usage
Custom Route Registration
import { createServer } from '@linkforty/core';
const server = await createServer({
database: { url: 'postgresql://localhost/linkforty' },
});
// Add custom routes
server.get('/custom', async (request, reply) => {
return { message: 'Hello World' };
});
await server.listen({ port: 3000 });Using Individual Route Handlers
import Fastify from 'fastify';
import { initializeDatabase, redirectRoutes, linkRoutes } from '@linkforty/core';
const fastify = Fastify();
// Initialize database separately
await initializeDatabase({ url: 'postgresql://localhost/linkforty' });
// Register only specific routes
await fastify.register(redirectRoutes);
await fastify.register(linkRoutes);
await fastify.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
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 -dImage 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 versionSee DOCKER.md for complete deployment guide including:
- Environment configuration
- Health checks
- Backup strategies
- Production best practices
Manual Deployment
For custom infrastructure needs:
- Install dependencies:
npm install @linkforty/core - Set up PostgreSQL database (13+)
- Set up Redis (optional but recommended)
- Run migrations:
npm run migrate - 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
userIdparameter 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
Set environment variables:
IOS_TEAM_ID=ABC123XYZ # Your Apple Developer Team ID IOS_BUNDLE_ID=com.yourcompany.yourappConfigure in Xcode:
- Add "Associated Domains" capability
- Add domain:
applinks:yourdomain.com
Verify AASA file:
curl https://yourdomain.com/.well-known/apple-app-site-associationExpected response:
{ "applinks": { "apps": [], "details": [ { "appID": "ABC123XYZ.com.yourcompany.yourapp", "paths": ["*"] } ] } }
Android App Links Setup
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-aliasSet 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:...Configure in AndroidManifest.xml:
<intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" /> <data android:host="yourdomain.com" /> </intent-filter>Verify assetlinks.json:
curl https://yourdomain.com/.well-known/assetlinks.jsonExpected 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
| Platform | Package | Install |
|----------|---------|---------|
| React Native | @linkforty/mobile-sdk-react-native | npm install @linkforty/mobile-sdk-react-native |
| Expo | @linkforty/mobile-sdk-expo | npx expo install @linkforty/mobile-sdk-expo |
| iOS (Swift) | LinkFortySDK | Swift Package Manager |
| Android (Kotlin) | LinkFortySDK | Gradle 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.
- Migrate from Branch.io
- Migrate from AppsFlyer OneLink
- Migrate from Firebase Dynamic Links (shut down August 2025)
- Migrate from Adjust
- Migrate from Kochava
- Migration overview and checklist
For AI Tools (llms.txt)
LinkForty provides machine-readable documentation for AI coding assistants (Claude, ChatGPT, Cursor, Copilot).
- Quick reference: docs.linkforty.com/llms.txt
- Complete integration guide: docs.linkforty.com/llms-full.txt
Download into your project for AI-assisted integration:
curl -o LINKFORTY.md https://docs.linkforty.com/llms-full.txtThe 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
- @linkforty/mobile-sdk-react-native - React Native SDK
- @linkforty/mobile-sdk-expo - Expo SDK
- mobile-sdk-ios - iOS SDK (Swift)
- mobile-sdk-android - Android SDK (Kotlin)
- LinkForty Cloud - Hosted SaaS version with authentication, teams, billing, and dashboard
Support
- Documentation: https://docs.linkforty.com
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with:
- Fastify - Fast web framework
- PostgreSQL - Powerful database
- Redis - In-memory cache
- Zod - TypeScript-first schema validation
- nanoid - Unique ID generation
- geoip-lite - IP geolocation
- ua-parser-js - User agent parsing
- qrcode - QR code generation
