Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ LINKFORTY_PORT=3000
CORS_ORIGIN=*

# -----------------------------------------------------------------------------
# Custom Domain Configuration (Optional)
# Short Link Domain (Optional)
# -----------------------------------------------------------------------------
# SERVICE_DOMAIN=yourdomain.com
# Base URL encoded in QR codes, including the scheme. Defaults to the request's host.
# SHORTLINK_DOMAIN=https://go.yourdomain.com

# -----------------------------------------------------------------------------
# Mobile SDK Configuration (Optional - for iOS Universal Links and Android App Links)
Expand Down
1 change: 0 additions & 1 deletion .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ The following environment variables should be kept secure:

- `DATABASE_URL` - Contains database credentials
- `REDIS_URL` - Contains Redis credentials (if used)
- `JWT_SECRET` - Used for token signing
- Any custom API keys or secrets

### Production Deployment
Expand Down
1 change: 0 additions & 1 deletion DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ docker run -d \
| `REDIS_URL` | No | - | Redis connection string (recommended) |
| `PORT` | No | `3000` | Server port |
| `NODE_ENV` | No | `production` | Environment mode |
| `JWT_SECRET` | No | - | JWT signing secret |
| `ASN_DB_PATH` | No | - | ASN database (`.mmdb`) path, such as DB-IP's IP to ASN Lite; turns on experimental email scanner detection (mount the file into the container) |

See [.env.example](.env.example) for complete configuration options.
Expand Down
201 changes: 1 addition & 200 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,208 +115,9 @@ docker run -d \

See [DOCKER.md](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).

```bash
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.

Install attribution settings, per link:

- `attributionWindowHours` (1–2160, default 168): how long after a click an install can still be matched to it.
- `attributionWindowMinutes` (5–129600): the same window in minutes, for windows under an hour. Send one or the other; when set it replaces the hours, and updating the link with `attributionWindowHours` clears it.
- `oneInstallPerClick` (default `false`): a click is matched to one install only. A second install that would have matched the same click is matched to another click, or recorded as organic. Suited to single-use links, such as sign-in links.

#### Get All Links

```bash
# Single-tenant (all links)
GET /api/links

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

#### Get a Specific Link

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

#### Update a Link

```bash
PUT /api/links/:id?userId=user-uuid
Content-Type: application/json

{
"title": "Updated Title",
"isActive": false
}
```

#### Duplicate a Link

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

#### Delete a Link

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

### Analytics

#### Get Analytics Overview

```bash
# 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

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

### Redirect

```bash
GET /:shortCode
GET /:templateSlug/:shortCode
```

Redirects users to the appropriate URL based on device type (iOS/Android/web), evaluates targeting rules, and tracks the click asynchronously. See [Redirects](docs/redirects.md) for the not-found page, the mobile interstitial and the Launchpad page.

### QR Codes

```bash
GET /api/links/:id/qr?format=png&size=300
GET /api/links/:id/qr?format=svg
GET /api/links/:id/qr?url=https://links.example.com/spring/abc123 # encode the link on a custom host / template path
```

Without `url` the code encodes `SHORTLINK_DOMAIN/{shortCode}`. `url` must be an http(s) URL whose path ends in the link's short code; anything else is a 400. The Launchpad page uses it to encode its own address, so a phone that scans the code lands on the same host and path the visitor is on.

### Webhooks

```bash
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.

Which webhooks hear about a link: one with an `organization_id` receives events for every link in that organization; one without receives events for links created by its `user_id`; one with neither receives events for links that have neither. To keep a delivery log, pass a function to `setWebhookDeliveryLogger()`. It is called with the webhook id, the attempt's result and the payload sent.

### Mobile SDK Endpoints

```bash
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
```

An event whose `installId` this server no longer has is **recovered, not
refused**: the install is recorded again (marked `recovered`, with no link,
click or confidence score, so it is never mistaken for a fresh attributed
install) and the event is stored against it.

This matters because the id lives in the app's storage for the life of the
install while the server's copy may not: a restore from an older backup, a
manual cleanup, an app build pointed at a fresh database, or an analytics
retention job all leave a live device holding an id nothing resolves. The
bundled SDKs store the id once and never re-register, so without recovery
that device is refused forever with no way back. The 404 that remains carries
`code: "INSTALL_NOT_FOUND"` and `action: "reregister"`.

### Health

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

### Debug & Testing

```bash
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

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

### OG Preview

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

## Reference

- [API](docs/api.md)
- [Redirects](docs/redirects.md)
- [Configuration](docs/configuration.md): server options and hooks, `linkforty.config.json`, reverse proxies, environment variables
- [Database schema](docs/database-schema.md)
Expand Down
13 changes: 5 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,12 @@ services:
TRUST_PROXY: ${TRUST_PROXY:-}
TRUSTED_CLIENT_IP_HEADER: ${TRUSTED_CLIENT_IP_HEADER:-}

# Optional: JWT secret for authentication
# JWT_SECRET: ${JWT_SECRET}
# Optional: base URL encoded in QR codes, including the scheme.
SHORTLINK_DOMAIN: ${SHORTLINK_DOMAIN:-}

# Optional: Email configuration
# SMTP_HOST: ${SMTP_HOST}
# SMTP_PORT: ${SMTP_PORT}
# SMTP_USER: ${SMTP_USER}
# SMTP_PASS: ${SMTP_PASS}
# EMAIL_FROM: ${EMAIL_FROM}
# Optional: trust the x-lf-bot header. Only behind a proxy that sets it
# and strips copies sent by clients.
TRUST_EDGE_BOT_HEADER: ${TRUST_EDGE_BOT_HEADER:-}

ports:
- "${LINKFORTY_PORT:-3000}:3000"
Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# LinkForty Core reference

Reference for running and embedding Core. These pages live with the code, so the copy in a release's git tag matches that release. For guides and the API, see [docs.linkforty.com](https://docs.linkforty.com).
Reference for running and embedding Core. These pages live with the code, so the copy in a release's git tag matches that release. For guides, see [docs.linkforty.com](https://docs.linkforty.com).

- [API](api.md): links, templates, analytics, QR codes, webhooks, SDK endpoints, health and debug routes
- [Redirects](redirects.md): how a request is routed, the not-found page, the mobile interstitial and the Launchpad page
- [Configuration](configuration.md): server options and hooks, `linkforty.config.json`, reverse proxies, email scanner detection and environment variables
- [Database schema](database-schema.md): tables, and how short codes resolve
Expand Down
Loading
Loading