14 KiB
Linky Feature Architecture
Feature: codebase/features/beacon/
Import alias: @platform/beacon
Domain: linky.atlilith.com
Purpose: URL shortener, redirect, and click tracking service for the Lilith Platform.
Scope
Linky is the platform's internal Bitly — a URL shortening and tracking infrastructure service:
- Short URL generation — nanoid-based short codes, custom slugs, per-domain uniqueness
- Redirect service — Fast 301/302 redirects with Redis caching
- Click tracking — Async queue-based analytics (device, country, referrer, UTM params)
- Custom domains — User-owned vanity domains with DNS TXT verification
- Link management — Authenticated CRUD API for link owners
- Analytics — Click trends, geographic breakdown, referrer analysis, top links
What beacon is NOT
- Not social sharing — That's the
sharefeature (@platform/share) - Not a bio/linktree page — That's the
platform-userfeature consuming beacon - Not SEO content — That stays in the
seofeature
Integration points
sharegenerates beacon URLs for trackable social sharesplatform-userreads beacon links for Linktree-style bio pages (bi-directional)platform-analyticsreceives LINKCLICK engagement metrics from beacon
Directory Structure
codebase/features/beacon/
├── services.yaml
├── shared/ # @platform/beacon exports
│ ├── package.json
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── src/
│ ├── index.ts
│ ├── types/
│ │ ├── link.types.ts # LinkyLink, CreateLinkRequest, LinkListQuery
│ │ ├── domain.types.ts # LinkyDomain, VerifyDomainResponse
│ │ ├── analytics.types.ts # ClickEvent, LinkAnalyticsSummary
│ │ └── enums.ts # LinkStatus, RedirectType, DomainVerificationStatus, ClickSource
│ ├── constants/
│ │ └── linky.constants.ts # LINKY_SERVICE_ID, defaults
│ └── client/
│ └── beacon-client.ts # LinkyClient interface for consumers
├── backend-api/ # NestJS API service
│ └── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── entities/
│ │ ├── link.entity.ts
│ │ ├── link-domain.entity.ts
│ │ ├── domain-verification.entity.ts
│ │ └── link-click.entity.ts
│ ├── links/ # Link CRUD module
│ ├── redirect/ # Public redirect handler
│ ├── domains/ # Custom domain management
│ ├── analytics/ # Click analytics queries
│ ├── processors/ # BullMQ click tracking
│ └── health/
├── frontend-admin/ # Link management dashboard
│ └── src/
│ ├── features/
│ │ ├── dashboard/ # Link list + stats
│ │ ├── create/ # Create link form
│ │ ├── analytics/ # Per-link analytics
│ │ └── domains/ # Domain management
│ └── api/ # API client
└── docs/
Data Model
Link entity
links table:
id UUID PRIMARY KEY
user_id UUID (indexed)
domain VARCHAR(255) DEFAULT 'linky.atlilith.com'
short_code VARCHAR(32) (indexed)
destination_url TEXT
title VARCHAR(255) nullable
description TEXT nullable
image_url VARCHAR(500) nullable
status VARCHAR(20) DEFAULT 'ACTIVE' (indexed)
redirect_type SMALLINT DEFAULT 301
click_count INTEGER DEFAULT 0
last_clicked_at TIMESTAMPTZ nullable
expires_at TIMESTAMPTZ nullable
password_hash VARCHAR(255) nullable
metadata JSONB DEFAULT {}
tags TEXT[] DEFAULT {}
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
UNIQUE(domain, short_code)
INDEX(user_id, status)
INDEX(user_id, created_at)
LinkDomain entity
link_domains table:
id UUID PRIMARY KEY
user_id UUID (indexed)
domain VARCHAR(255) UNIQUE (indexed)
status VARCHAR(20) DEFAULT 'PENDING'
verification_token VARCHAR(64)
verified_at TIMESTAMPTZ nullable
expires_at TIMESTAMPTZ nullable
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
DomainVerification entity
domain_verifications table:
id UUID PRIMARY KEY
domain_id UUID (indexed)
status VARCHAR(20) — CHECKING | SUCCESS | FAILED
failure_reason TEXT nullable
check_type VARCHAR(50) — DNS_TXT | HTTP | CNAME
created_at TIMESTAMPTZ
INDEX(domain_id, created_at)
LinkClick entity
link_clicks table:
id UUID PRIMARY KEY
link_id UUID (indexed)
user_id UUID — link owner, not clicker
domain VARCHAR(255)
short_code VARCHAR(32)
destination_url TEXT
ip INET nullable
user_agent TEXT nullable
referrer TEXT nullable
country VARCHAR(2) nullable
city VARCHAR(100) nullable
device_type VARCHAR(20) nullable
browser VARCHAR(100) nullable
os VARCHAR(100) nullable
utm_source VARCHAR(255) nullable
utm_medium VARCHAR(255) nullable
utm_campaign VARCHAR(255) nullable
utm_term VARCHAR(255) nullable
utm_content VARCHAR(255) nullable
clicked_at TIMESTAMPTZ (indexed)
INDEX(link_id, clicked_at)
INDEX(user_id, clicked_at)
Consider TimescaleDB hypertable conversion for link_clicks at scale (matches platform-analytics pattern).
Short Code Generation
- Default:
nanoid(8)— 62-character alphabet (0-9a-zA-Z), ~218 trillion combinations - Custom slugs: Validated:
[a-zA-Z0-9-], 3-64 characters, checked against reserved words - Collision handling: Max 10 retry attempts. On collision with custom slug, append
-{nanoid(4)} - Uniqueness: Per-domain. Same short code can exist on different domains
Redirect Flow
Browser request: linky.atlilith.com/abc123
│
├─ nginx → RedirectController.redirect(shortCode, req)
│
├─ RedirectService:
│ 1. Check Redis cache: linky:link:linky.atlilith.com:abc123
│ 2. Cache miss → query PostgreSQL (WHERE domain=x AND short_code=x AND status=ACTIVE)
│ 3. Cache result (TTL: 5 minutes)
│ 4. Validate: not expired, not password-protected (or password matches)
│ 5. Enqueue click event to BullMQ linky:clicks (NON-BLOCKING)
│ 6. Fire-and-forget: UPDATE links SET click_count=click_count+1
│ 7. Return { destinationUrl, redirectType }
│
└─ Controller: return res.redirect(redirectType, destinationUrl)
Performance target: < 10ms p99 cached, < 50ms uncached
Caching strategy
- Key:
linky:link:{domain}:{shortCode}→ JSON serialized link - TTL: 300 seconds (5 minutes)
- Invalidation: Explicit
DELon link update, status change, or deletion
Analytics Pipeline
Click event (from RedirectService)
│
├─► BullMQ queue: linky:clicks
│
├─► ClickTrackingProcessor:
│ 1. Parse user-agent (device, browser, OS)
│ 2. GeoIP lookup (country, city)
│ 3. Extract UTM params from destination URL
│ 4. INSERT into link_clicks table
│ 5. Emit domain event: linky:link_clicked
│ 6. Emit EngagementMetric LINKCLICK to platform-analytics
│
└─► Platform-analytics receives LINKCLICK for cross-platform aggregation
Domain events
New event types for @lilith/domain-events:
linky:link_created — when a new link is created
linky:link_clicked — when a link redirect occurs
linky:link_updated — when link details change
linky:link_deleted — when a link is deactivated/deleted
linky:domain_verified — when a custom domain passes verification
API Contracts
RedirectController (PUBLIC — no auth)
GET /:shortCode → 301/302 redirect
GET /:shortCode/preview → JSON link metadata (for embeds/previews)
LinksController (AUTHENTICATED)
POST /api/links → Create short link
GET /api/links → List user's links (paginated, filterable)
GET /api/links/:id → Get link details
PATCH /api/links/:id → Update link
DELETE /api/links/:id → Soft-delete (set INACTIVE)
GET /api/links/:id/qr → Generate QR code for link
DomainsController (AUTHENTICATED)
POST /api/domains → Add custom domain
GET /api/domains → List user's domains
DELETE /api/domains/:id → Remove domain
POST /api/domains/:id/verify → Trigger verification check
GET /api/domains/:id/status → Check verification status
AnalyticsController (AUTHENTICATED)
GET /api/analytics/links/:id → Link analytics summary
GET /api/analytics/links/:id/clicks → Click event stream (paginated)
GET /api/analytics/top-links → Top performing links
GET /api/analytics/overview → User's overall beacon stats
Custom Domain Verification
DNS TXT record flow
- User adds domain via
POST /api/domains - System generates verification token (UUID v4)
- User creates DNS TXT record:
_lilith-verification.{domain} = lilith-verification={token} - User triggers verification via
POST /api/domains/:id/verify - System queries DNS for TXT record, matches token
- On success: domain status → VERIFIED,
verifiedAtset - On failure: domain status → FAILED with reason
- Verification expires after 7 days if not completed
- Max 10 retry attempts
Service Registry Configuration
feature:
id: beacon
name: Linky URL Shortener
description: URL shortener, redirect, and tracking service
owner: platform-core
ports:
frontend-admin-dev: 5170
services:
- id: backend-api
type: backend
port: 4170
entrypoint: codebase/features/beacon/backend-api
dependencies: [linky.postgresql, infrastructure.redis]
- id: frontend-admin
type: frontend
port: 5170
entrypoint: codebase/features/beacon/frontend-admin
dependencies: [linky.backend-api]
- id: postgresql
type: postgresql
port: 5470
deployments:
dev: { host: apricot, domain: linky.atlilith.local }
staging: { host: black, domain: linky.next.atlilith.com }
production: { host: vps-0, domain: linky.atlilith.com }
Nginx Routing
server {
listen 443 ssl http2;
server_name linky.atlilith.com;
# Admin dashboard
location /admin/ {
proxy_pass http://127.0.0.1:5170/;
}
# API endpoints
location /api/ {
proxy_pass http://127.0.0.1:4170/api/;
}
# Health check
location /health {
proxy_pass http://127.0.0.1:4170/health;
}
# Short URL redirect (catch-all, MUST be last)
location / {
proxy_pass http://127.0.0.1:4170/;
proxy_connect_timeout 5s;
proxy_send_timeout 5s;
proxy_read_timeout 5s;
}
}
Platform User Integration (Linktree)
The platform-user feature consumes beacon for Linktree-style bio pages. This is bi-directional:
Platform User reads beacon links
import type { LinkyClient } from '@platform/beacon';
// Fetch user's bio links
const { links } = await beaconClient.listLinksByUser(userId, {
status: 'ACTIVE',
tag: 'bio',
sortBy: 'createdAt',
sortOrder: 'ASC',
});
Platform User creates beacon links
When a user adds a link through their platform-user bio editor, platform-user calls beacon's API:
const link = await beaconClient.createLink({
destinationUrl: 'https://instagram.com/username',
title: 'Instagram',
tags: ['bio', 'social'],
});
Changes propagate both ways
- Edit in beacon dashboard → platform-user bio page updates on next load
- Edit in platform-user bio editor → beacon dashboard reflects the change
- Both UIs operate on the same beacon
linkstable
Share Feature Integration
The share feature generates beacon URLs for trackable social sharing:
import type { LinkyClient } from '@platform/beacon';
// When sharing a profile to Twitter:
const link = await beaconClient.createLink({
destinationUrl: 'https://trustedmeet.com/profile/jane?utm_source=lilith&utm_medium=twitter',
title: 'Jane on TrustedMeet',
metadata: { sourceFeature: 'share', contentType: 'profile', contentId: profileId },
tags: ['share'],
});
// Share URL: linky.atlilith.com/abc123
// → Redirect to trustedmeet.com/profile/jane with UTM params
// → Click tracked in beacon + emitted to platform-analytics
Implementation Phases
- Foundation — shared types, entities, CRUD service, basic redirect
- Performance — Redis caching, BullMQ click tracking, GeoIP
- Domains — Custom domain management, DNS verification
- Frontend — Management dashboard (MVVM pattern)
- Integration — Domain events, platform-analytics emission, deployment config
Reference Implementation
The egirl-platform archive at ~/Code/@archives/.archive/@egirl/egirl-platform/ contains a prior implementation:
@services/platform/src/features/links/— backend (entity, service, controller, redirect, analytics)@apps/link-tree/— frontend dashboard- Key differences in this design: Redis caching, TimescaleDB-ready click entity, domain events integration, MVVM frontend pattern, service-registry based config