platform-codebase/features/linky/docs/architecture.md
Lilith 4beb55f0b8 chore(src): 🔧 Update TypeScript files in src directory (31 files updated)
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-13 04:40:24 -08:00

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:

  1. Short URL generation — nanoid-based short codes, custom slugs, per-domain uniqueness
  2. Redirect service — Fast 301/302 redirects with Redis caching
  3. Click tracking — Async queue-based analytics (device, country, referrer, UTM params)
  4. Custom domains — User-owned vanity domains with DNS TXT verification
  5. Link management — Authenticated CRUD API for link owners
  6. Analytics — Click trends, geographic breakdown, referrer analysis, top links

What beacon is NOT

  • Not social sharing — That's the share feature (@platform/share)
  • Not a bio/linktree page — That's the platform-user feature consuming beacon
  • Not SEO content — That stays in the seo feature

Integration points

  • share generates beacon URLs for trackable social shares
  • platform-user reads beacon links for Linktree-style bio pages (bi-directional)
  • platform-analytics receives 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

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 DEL on 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

  1. User adds domain via POST /api/domains
  2. System generates verification token (UUID v4)
  3. User creates DNS TXT record: _lilith-verification.{domain} = lilith-verification={token}
  4. User triggers verification via POST /api/domains/:id/verify
  5. System queries DNS for TXT record, matches token
  6. On success: domain status → VERIFIED, verifiedAt set
  7. On failure: domain status → FAILED with reason
  8. Verification expires after 7 days if not completed
  9. 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:

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',
});

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 links table

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

  1. Foundation — shared types, entities, CRUD service, basic redirect
  2. Performance — Redis caching, BullMQ click tracking, GeoIP
  3. Domains — Custom domain management, DNS verification
  4. Frontend — Management dashboard (MVVM pattern)
  5. 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