@linkforty/core 1.5.1 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt ADDED
@@ -0,0 +1,763 @@
1
+ # @linkforty/core
2
+
3
+ > Open-source deeplink management engine built on Fastify + PostgreSQL + optional Redis. Provides smart link routing with device detection, click analytics, UTM tracking, deferred deep linking, device fingerprinting, webhooks, QR codes, link templates, and mobile SDK endpoints. No auth included — bring your own. Install via npm, connect to PostgreSQL, and you have a full deep linking platform. Licensed AGPL-3.0-only.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @linkforty/core
9
+ ```
10
+
11
+ Requires:
12
+ - Node.js 20+
13
+ - PostgreSQL 14+
14
+ - Redis (optional, recommended for caching)
15
+
16
+ ## Quick Start
17
+
18
+ ```typescript
19
+ import { createServer } from '@linkforty/core';
20
+
21
+ const server = await createServer({
22
+ database: {
23
+ url: 'postgresql://postgres:password@localhost:5432/linkforty',
24
+ pool: { min: 2, max: 10 },
25
+ },
26
+ redis: {
27
+ url: 'redis://localhost:6379', // optional
28
+ },
29
+ cors: {
30
+ origin: ['https://yourdomain.com'],
31
+ },
32
+ logger: true,
33
+ });
34
+
35
+ await server.listen({ port: 3000, host: '0.0.0.0' });
36
+ ```
37
+
38
+ That's it. The server auto-creates all database tables on first startup, registers all API routes, and starts serving.
39
+
40
+ ## Docker Quick Start
41
+
42
+ ```yaml
43
+ # docker-compose.yml
44
+ services:
45
+ postgres:
46
+ image: postgres:15
47
+ environment:
48
+ POSTGRES_DB: linkforty
49
+ POSTGRES_USER: linkforty
50
+ POSTGRES_PASSWORD: changeme
51
+ ports:
52
+ - "5432:5432"
53
+ volumes:
54
+ - pgdata:/var/lib/postgresql/data
55
+
56
+ redis:
57
+ image: redis:7-alpine
58
+ ports:
59
+ - "6379:6379"
60
+
61
+ linkforty:
62
+ image: node:20-alpine
63
+ working_dir: /app
64
+ command: node dist/index.js
65
+ ports:
66
+ - "3000:3000"
67
+ environment:
68
+ DATABASE_URL: postgresql://linkforty:changeme@postgres:5432/linkforty
69
+ REDIS_URL: redis://redis:6379
70
+ PORT: 3000
71
+ NODE_ENV: production
72
+ depends_on:
73
+ - postgres
74
+ - redis
75
+
76
+ volumes:
77
+ pgdata:
78
+ ```
79
+
80
+ ```bash
81
+ docker compose up -d
82
+ curl http://localhost:3000/health
83
+ ```
84
+
85
+ ## Configuration
86
+
87
+ ### createServer(options)
88
+
89
+ ```typescript
90
+ interface ServerOptions {
91
+ database?: {
92
+ url?: string; // PostgreSQL connection string
93
+ pool?: {
94
+ min?: number; // Default: 2
95
+ max?: number; // Default: 10
96
+ };
97
+ };
98
+ redis?: {
99
+ url: string; // Redis connection string
100
+ };
101
+ cors?: {
102
+ origin: string | string[];
103
+ };
104
+ logger?: boolean; // Enable Fastify logger
105
+ }
106
+ ```
107
+
108
+ ### Environment Variables
109
+
110
+ ```bash
111
+ # Database (PostgreSQL)
112
+ DATABASE_URL=postgresql://linkforty:changeme@localhost:5432/linkforty
113
+
114
+ # Redis (optional — falls back to database on miss or error)
115
+ REDIS_URL=redis://localhost:6379
116
+
117
+ # Server
118
+ PORT=3000
119
+ HOST=0.0.0.0
120
+ NODE_ENV=production
121
+ CORS_ORIGIN=*
122
+
123
+ # Short link domain (used in QR codes and SDK responses)
124
+ SHORTLINK_DOMAIN=https://go.yourdomain.com
125
+
126
+ # iOS Universal Links (optional)
127
+ IOS_TEAM_ID=ABC123XYZ
128
+ IOS_BUNDLE_ID=com.yourcompany.yourapp
129
+
130
+ # Android App Links (optional)
131
+ ANDROID_PACKAGE_NAME=com.yourcompany.yourapp
132
+ ANDROID_SHA256_FINGERPRINTS=AA:BB:CC:DD:...
133
+ ```
134
+
135
+ ## Module Exports
136
+
137
+ ```typescript
138
+ import { createServer } from '@linkforty/core'; // Server factory
139
+ import { generateShortCode } from '@linkforty/core/utils'; // Utility functions
140
+ import { db } from '@linkforty/core/database'; // PostgreSQL pool
141
+ import { linkRoutes } from '@linkforty/core/routes'; // Fastify route plugins
142
+ import type { Link, AnalyticsData } from '@linkforty/core/types'; // TypeScript types
143
+ ```
144
+
145
+ ## TypeScript Types
146
+
147
+ ### Link
148
+
149
+ ```typescript
150
+ interface Link {
151
+ id: string; // UUID
152
+ userId?: string; // Optional (multi-tenant scoping)
153
+ template_id?: string;
154
+ template_slug?: string;
155
+ short_code: string; // Unique, immutable after creation
156
+ original_url: string;
157
+ title?: string;
158
+ description?: string;
159
+
160
+ // Platform-specific URLs
161
+ ios_app_store_url?: string;
162
+ android_app_store_url?: string;
163
+ web_fallback_url?: string;
164
+
165
+ // Deep linking
166
+ app_scheme?: string; // URI scheme (e.g., "myapp")
167
+ ios_universal_link?: string;
168
+ android_app_link?: string;
169
+ deep_link_path?: string; // In-app destination (e.g., "/product/123")
170
+ deep_link_parameters?: Record<string, any>;
171
+
172
+ // Analytics
173
+ utmParameters?: UTMParameters;
174
+ targeting_rules?: TargetingRules;
175
+
176
+ // Social preview (Open Graph)
177
+ og_title?: string;
178
+ og_description?: string;
179
+ og_image_url?: string;
180
+ og_type?: string;
181
+
182
+ // Lifecycle
183
+ attribution_window_hours?: number; // Default: 168 (7 days)
184
+ is_active: boolean;
185
+ expires_at?: string;
186
+ created_at: string;
187
+ updated_at: string;
188
+ click_count?: number;
189
+ }
190
+
191
+ interface UTMParameters {
192
+ source?: string;
193
+ medium?: string;
194
+ campaign?: string;
195
+ term?: string;
196
+ content?: string;
197
+ }
198
+
199
+ interface TargetingRules {
200
+ countries?: string[]; // ISO country codes (e.g., ["US", "GB"])
201
+ devices?: ('ios' | 'android' | 'web')[];
202
+ languages?: string[]; // BCP 47 codes (e.g., ["en", "es"])
203
+ }
204
+ ```
205
+
206
+ ### Create/Update Link Request
207
+
208
+ ```typescript
209
+ interface CreateLinkRequest {
210
+ userId?: string;
211
+ templateId?: string;
212
+ originalUrl: string; // Required, must be valid URL
213
+ title?: string;
214
+ description?: string;
215
+ iosAppStoreUrl?: string;
216
+ androidAppStoreUrl?: string;
217
+ webFallbackUrl?: string;
218
+ appScheme?: string;
219
+ iosUniversalLink?: string;
220
+ androidAppLink?: string;
221
+ deepLinkPath?: string;
222
+ deepLinkParameters?: Record<string, any>;
223
+ utmParameters?: UTMParameters;
224
+ targetingRules?: TargetingRules;
225
+ ogTitle?: string;
226
+ ogDescription?: string;
227
+ ogImageUrl?: string;
228
+ ogType?: string;
229
+ attributionWindowHours?: number; // 1-2160
230
+ customCode?: string; // Custom short code (auto-generated if omitted)
231
+ expiresAt?: string; // ISO 8601 datetime
232
+ }
233
+
234
+ // UpdateLinkRequest is Partial<CreateLinkRequest> plus:
235
+ interface UpdateLinkRequest extends Partial<CreateLinkRequest> {
236
+ isActive?: boolean;
237
+ }
238
+ ```
239
+
240
+ ### Link Template
241
+
242
+ ```typescript
243
+ interface LinkTemplate {
244
+ id: string;
245
+ userId?: string;
246
+ name: string;
247
+ slug: string; // Auto-generated 8-char alphanumeric
248
+ description?: string;
249
+ settings: LinkTemplateSettings;
250
+ is_default: boolean;
251
+ created_at: string;
252
+ updated_at: string;
253
+ }
254
+
255
+ interface LinkTemplateSettings {
256
+ defaultIosUrl?: string;
257
+ defaultAndroidUrl?: string;
258
+ defaultWebFallbackUrl?: string;
259
+ defaultAttributionWindowHours?: number;
260
+ utmParameters?: UTMParameters;
261
+ targetingRules?: TargetingRules;
262
+ expiresAfterDays?: number;
263
+ }
264
+ ```
265
+
266
+ ### Analytics
267
+
268
+ ```typescript
269
+ interface AnalyticsData {
270
+ totalClicks: number;
271
+ uniqueClicks: number;
272
+ clicksByDate: Array<{ date: string; clicks: number }>;
273
+ clicksByCountry: Array<{ country: string; countryCode: string; clicks: number }>;
274
+ clicksByDevice: Array<{ device: string; clicks: number }>;
275
+ clicksByPlatform: Array<{ platform: string; clicks: number }>;
276
+ clicksByBrowser: Array<{ browser: string; clicks: number }>;
277
+ clicksByHour: Array<{ hour: number; clicks: number }>;
278
+ clicksByUtmSource: Array<{ source: string; clicks: number }>;
279
+ clicksByUtmMedium: Array<{ medium: string; clicks: number }>;
280
+ clicksByUtmCampaign: Array<{ campaign: string; clicks: number }>;
281
+ clicksByReferrer: Array<{ source: string; clicks: number }>;
282
+ topLinks: Array<{
283
+ id: string;
284
+ shortCode: string;
285
+ title: string | null;
286
+ originalUrl: string;
287
+ totalClicks: number;
288
+ uniqueClicks: number;
289
+ }>;
290
+ }
291
+ ```
292
+
293
+ ### Webhook
294
+
295
+ ```typescript
296
+ type WebhookEvent = 'click_event' | 'install_event' | 'conversion_event';
297
+
298
+ interface Webhook {
299
+ id: string;
300
+ user_id?: string;
301
+ name: string;
302
+ url: string;
303
+ secret: string; // Auto-generated, HMAC-SHA256 signing key
304
+ events: WebhookEvent[];
305
+ is_active: boolean;
306
+ retry_count: number; // 1-10
307
+ timeout_ms: number; // 1000-60000
308
+ headers: Record<string, string>;
309
+ created_at: string;
310
+ updated_at: string;
311
+ }
312
+ ```
313
+
314
+ ## API Endpoints
315
+
316
+ All endpoints accept JSON request/response bodies. The `userId` query parameter is optional on all routes — when provided, queries are scoped to that user (multi-tenant mode). When omitted, all records are accessible (single-tenant mode).
317
+
318
+ ### Link Management
319
+
320
+ #### POST /api/links — Create Link
321
+
322
+ ```bash
323
+ curl -X POST http://localhost:3000/api/links \
324
+ -H "Content-Type: application/json" \
325
+ -d '{
326
+ "originalUrl": "https://example.com/product/789",
327
+ "title": "Summer Campaign",
328
+ "iosAppStoreUrl": "https://apps.apple.com/app/id123456789",
329
+ "androidAppStoreUrl": "https://play.google.com/store/apps/details?id=com.example.app",
330
+ "webFallbackUrl": "https://example.com/product/789",
331
+ "deepLinkParameters": { "route": "product", "productId": "789" },
332
+ "utmParameters": { "source": "instagram", "medium": "social", "campaign": "summer" },
333
+ "templateId": "550e8400-e29b-41d4-a716-446655440000"
334
+ }'
335
+ ```
336
+
337
+ Response (201):
338
+
339
+ ```json
340
+ {
341
+ "id": "uuid",
342
+ "shortCode": "abc12345",
343
+ "originalUrl": "https://example.com/product/789",
344
+ "title": "Summer Campaign",
345
+ "deepLinkParameters": { "route": "product", "productId": "789" },
346
+ "utmParameters": { "source": "instagram", "medium": "social", "campaign": "summer" },
347
+ "isActive": true,
348
+ "clickCount": 0,
349
+ "createdAt": "2025-01-15T10:30:00Z",
350
+ "updatedAt": "2025-01-15T10:30:00Z"
351
+ }
352
+ ```
353
+
354
+ #### GET /api/links — List Links
355
+
356
+ ```bash
357
+ curl "http://localhost:3000/api/links?userId=optional-user-id"
358
+ ```
359
+
360
+ #### GET /api/links/:id — Get Link
361
+
362
+ ```bash
363
+ curl "http://localhost:3000/api/links/uuid"
364
+ ```
365
+
366
+ #### PUT /api/links/:id — Update Link
367
+
368
+ ```bash
369
+ curl -X PUT "http://localhost:3000/api/links/uuid" \
370
+ -H "Content-Type: application/json" \
371
+ -d '{ "title": "Updated Title", "isActive": false }'
372
+ ```
373
+
374
+ #### DELETE /api/links/:id — Delete Link
375
+
376
+ ```bash
377
+ curl -X DELETE "http://localhost:3000/api/links/uuid"
378
+ ```
379
+
380
+ Response: `{ "success": true }`
381
+
382
+ #### POST /api/links/:id/duplicate — Clone Link
383
+
384
+ ```bash
385
+ curl -X POST "http://localhost:3000/api/links/uuid/duplicate"
386
+ ```
387
+
388
+ ### Templates
389
+
390
+ #### POST /api/templates — Create Template
391
+
392
+ ```bash
393
+ curl -X POST http://localhost:3000/api/templates \
394
+ -H "Content-Type: application/json" \
395
+ -d '{
396
+ "name": "E-commerce Links",
397
+ "settings": {
398
+ "defaultIosUrl": "https://apps.apple.com/app/id123",
399
+ "defaultAndroidUrl": "https://play.google.com/store/apps/details?id=com.example",
400
+ "defaultWebFallbackUrl": "https://example.com",
401
+ "defaultAttributionWindowHours": 168,
402
+ "utmParameters": { "source": "app", "medium": "deeplink" }
403
+ },
404
+ "isDefault": true
405
+ }'
406
+ ```
407
+
408
+ #### GET /api/templates — List Templates
409
+ #### GET /api/templates/:id — Get Template
410
+ #### PUT /api/templates/:id — Update Template
411
+ #### DELETE /api/templates/:id — Delete Template
412
+ #### PUT /api/templates/:id/set-default — Set Default Template
413
+
414
+ ### Analytics
415
+
416
+ #### GET /api/analytics/overview — Aggregate Analytics
417
+
418
+ ```bash
419
+ curl "http://localhost:3000/api/analytics/overview?days=30"
420
+ ```
421
+
422
+ Returns full `AnalyticsData` object (see types above).
423
+
424
+ #### GET /api/analytics/links/:linkId — Link-Specific Analytics
425
+
426
+ ```bash
427
+ curl "http://localhost:3000/api/analytics/links/uuid?days=7"
428
+ ```
429
+
430
+ ### Webhooks
431
+
432
+ #### POST /api/webhooks — Create Webhook
433
+
434
+ ```bash
435
+ curl -X POST http://localhost:3000/api/webhooks \
436
+ -H "Content-Type: application/json" \
437
+ -d '{
438
+ "name": "My Webhook",
439
+ "url": "https://example.com/webhooks/linkforty",
440
+ "events": ["click_event", "install_event", "conversion_event"],
441
+ "retryCount": 3,
442
+ "timeoutMs": 10000
443
+ }'
444
+ ```
445
+
446
+ Response includes auto-generated `secret` for HMAC verification.
447
+
448
+ #### GET /api/webhooks — List Webhooks
449
+ #### GET /api/webhooks/:id — Get Webhook (includes secret)
450
+ #### PUT /api/webhooks/:id — Update Webhook
451
+ #### DELETE /api/webhooks/:id — Delete Webhook
452
+ #### POST /api/webhooks/:id/test — Send Test Payload
453
+
454
+ **Webhook signature verification:**
455
+
456
+ ```typescript
457
+ import crypto from 'crypto';
458
+
459
+ function verifyWebhook(body: string, signature: string, secret: string): boolean {
460
+ const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
461
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(`sha256=${expected}`));
462
+ }
463
+ // Signature header: X-LinkForty-Signature
464
+ ```
465
+
466
+ ### Redirects (Public)
467
+
468
+ #### GET /:shortCode — Follow Short Link
469
+
470
+ Redirects (302) based on device:
471
+ 1. iOS device → `ios_app_store_url`
472
+ 2. Android device → `android_app_store_url`
473
+ 3. Desktop/other → `web_fallback_url` → `original_url`
474
+
475
+ UTM parameters are appended to the redirect URL. Click is tracked asynchronously.
476
+
477
+ #### GET /:templateSlug/:shortCode — Template-Based Redirect
478
+
479
+ Same behavior, resolves via template slug + short code.
480
+
481
+ ### QR Codes
482
+
483
+ #### GET /api/links/:id/qr — Generate QR Code
484
+
485
+ ```bash
486
+ # PNG (default)
487
+ curl "http://localhost:3000/api/links/uuid/qr" -o qr.png
488
+
489
+ # SVG
490
+ curl "http://localhost:3000/api/links/uuid/qr?format=svg" -o qr.svg
491
+ ```
492
+
493
+ ### Mobile SDK Endpoints (Public)
494
+
495
+ These are called by the LinkForty mobile SDKs (`@linkforty/mobile-sdk-react-native`, `@linkforty/mobile-sdk-expo`, iOS SDK, Android SDK).
496
+
497
+ #### POST /api/sdk/v1/install — Report App Install
498
+
499
+ ```bash
500
+ curl -X POST http://localhost:3000/api/sdk/v1/install \
501
+ -H "Content-Type: application/json" \
502
+ -d '{
503
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
504
+ "timezone": "America/New_York",
505
+ "language": "en-US",
506
+ "screenWidth": 390,
507
+ "screenHeight": 844,
508
+ "platform": "iOS",
509
+ "platformVersion": "17.0",
510
+ "attributionWindowHours": 168
511
+ }'
512
+ ```
513
+
514
+ Response:
515
+
516
+ ```json
517
+ {
518
+ "installId": "uuid",
519
+ "attributed": true,
520
+ "confidenceScore": 85,
521
+ "matchedFactors": ["ip", "user_agent", "timezone", "language", "screen"],
522
+ "deepLinkData": {
523
+ "shortCode": "abc123",
524
+ "originalUrl": "https://example.com/product/789",
525
+ "iosUrl": "https://apps.apple.com/app/id123",
526
+ "androidUrl": "https://play.google.com/store/apps/details?id=com.example",
527
+ "webFallbackUrl": "https://example.com/product/789",
528
+ "utmParameters": { "source": "instagram" },
529
+ "deepLinkParameters": { "route": "product", "productId": "789" }
530
+ }
531
+ }
532
+ ```
533
+
534
+ #### GET /api/sdk/v1/resolve/:shortCode — Resolve Deep Link Data
535
+
536
+ ```bash
537
+ curl "http://localhost:3000/api/sdk/v1/resolve/abc123?fp_tz=America/New_York&fp_platform=ios"
538
+ ```
539
+
540
+ Response:
541
+
542
+ ```json
543
+ {
544
+ "shortCode": "abc123",
545
+ "linkId": "uuid",
546
+ "deepLinkPath": "/product/789",
547
+ "appScheme": "myapp",
548
+ "iosUrl": "https://apps.apple.com/app/id123",
549
+ "androidUrl": "https://play.google.com/store/apps/details?id=com.example",
550
+ "webUrl": "https://example.com/product/789",
551
+ "utmParameters": { "source": "instagram" },
552
+ "customParameters": { "route": "product", "productId": "789" },
553
+ "clickedAt": "2025-01-15T10:30:00Z"
554
+ }
555
+ ```
556
+
557
+ #### POST /api/sdk/v1/event — Track In-App Event
558
+
559
+ ```bash
560
+ curl -X POST http://localhost:3000/api/sdk/v1/event \
561
+ -H "Content-Type: application/json" \
562
+ -d '{
563
+ "installId": "uuid",
564
+ "eventName": "purchase",
565
+ "eventData": { "amount": 29.99, "currency": "USD" }
566
+ }'
567
+ ```
568
+
569
+ Response: `{ "eventId": "uuid", "acknowledged": true }`
570
+
571
+ ### Well-Known (Auto-Served)
572
+
573
+ #### GET /.well-known/apple-app-site-association
574
+
575
+ Serves the AASA file for iOS Universal Links. Configure via `IOS_TEAM_ID` and `IOS_BUNDLE_ID` env vars.
576
+
577
+ #### GET /.well-known/assetlinks.json
578
+
579
+ Serves Digital Asset Links for Android App Links. Configure via `ANDROID_PACKAGE_NAME` and `ANDROID_SHA256_FINGERPRINTS` env vars.
580
+
581
+ ## Redirect Flow
582
+
583
+ When a user clicks a short link (e.g., `https://go.yourdomain.com/abc123`):
584
+
585
+ 1. Check Redis cache (`link:{shortCode}`, 5-min TTL)
586
+ 2. Fall back to PostgreSQL if cache miss or Redis unavailable
587
+ 3. Evaluate targeting rules (countries, devices, languages) — return 404 if no match
588
+ 4. Track click asynchronously (does not block redirect)
589
+ 5. Select destination URL based on device (iOS → Android → web → original)
590
+ 6. Append UTM parameters to destination URL
591
+ 7. Return 302 redirect
592
+
593
+ ## Fingerprint Attribution
594
+
595
+ When a mobile SDK reports an install, Core matches it to a previous click using probabilistic fingerprinting:
596
+
597
+ | Factor | Weight |
598
+ |--------|--------|
599
+ | IP address | 40 points |
600
+ | User agent | 30 points |
601
+ | Timezone | 10 points |
602
+ | Language | 10 points |
603
+ | Screen resolution | 10 points |
604
+
605
+ A match requires 70+ confidence score out of 100. Default attribution window is 168 hours (7 days), configurable per link (1-2160 hours).
606
+
607
+ ## Database Schema
608
+
609
+ Tables are auto-created on first startup by `initializeDatabase()`. No separate migration step needed.
610
+
611
+ | Table | Purpose |
612
+ |-------|---------|
613
+ | `link_templates` | Reusable link configuration templates |
614
+ | `links` | Short links with all configuration |
615
+ | `click_events` | Click analytics with geolocation |
616
+ | `device_fingerprints` | Fingerprint components for attribution matching |
617
+ | `install_events` | Mobile app install tracking |
618
+ | `in_app_events` | Conversion events from mobile apps |
619
+ | `webhooks` | Webhook endpoint configurations |
620
+
621
+ All tables use UUID primary keys (`gen_random_uuid()`), `created_at`/`updated_at` timestamps, and snake_case column names.
622
+
623
+ ## Extending Core
624
+
625
+ Core returns a standard Fastify instance, so you can add custom routes, plugins, and hooks:
626
+
627
+ ```typescript
628
+ import { createServer } from '@linkforty/core';
629
+
630
+ const server = await createServer({
631
+ database: { url: process.env.DATABASE_URL },
632
+ logger: true,
633
+ });
634
+
635
+ // Add custom authentication
636
+ server.addHook('onRequest', async (request, reply) => {
637
+ const apiKey = request.headers['x-api-key'];
638
+ if (!apiKey || apiKey !== process.env.API_KEY) {
639
+ reply.code(401).send({ error: 'Unauthorized' });
640
+ }
641
+ });
642
+
643
+ // Add custom routes
644
+ server.get('/api/custom/stats', async (request, reply) => {
645
+ const { db } = await import('@linkforty/core/database');
646
+ const result = await db.query('SELECT COUNT(*) FROM links');
647
+ return { totalLinks: parseInt(result.rows[0].count) };
648
+ });
649
+
650
+ await server.listen({ port: 3000, host: '0.0.0.0' });
651
+ ```
652
+
653
+ ## Complete Self-Hosted Server Example
654
+
655
+ ```typescript
656
+ // server.ts
657
+ import 'dotenv/config';
658
+ import { createServer } from '@linkforty/core';
659
+
660
+ async function start() {
661
+ const server = await createServer({
662
+ database: {
663
+ url: process.env.DATABASE_URL || 'postgresql://linkforty:changeme@localhost:5432/linkforty',
664
+ pool: { min: 2, max: 10 },
665
+ },
666
+ redis: process.env.REDIS_URL
667
+ ? { url: process.env.REDIS_URL }
668
+ : undefined,
669
+ cors: {
670
+ origin: process.env.CORS_ORIGIN?.split(',') || ['*'],
671
+ },
672
+ logger: true,
673
+ });
674
+
675
+ const port = parseInt(process.env.PORT || '3000');
676
+ const host = process.env.HOST || '0.0.0.0';
677
+
678
+ await server.listen({ port, host });
679
+ console.log(`LinkForty Core running at http://${host}:${port}`);
680
+ }
681
+
682
+ start().catch((err) => {
683
+ console.error('Failed to start:', err);
684
+ process.exit(1);
685
+ });
686
+ ```
687
+
688
+ ```json
689
+ // package.json
690
+ {
691
+ "type": "module",
692
+ "scripts": {
693
+ "start": "node --loader tsx server.ts",
694
+ "dev": "tsx watch server.ts"
695
+ },
696
+ "dependencies": {
697
+ "@linkforty/core": "^1.6.0",
698
+ "dotenv": "^16.3.0"
699
+ },
700
+ "devDependencies": {
701
+ "tsx": "^4.0.0",
702
+ "typescript": "^5.2.0"
703
+ }
704
+ }
705
+ ```
706
+
707
+ ## Utility Functions
708
+
709
+ ```typescript
710
+ import {
711
+ generateShortCode,
712
+ parseUserAgent,
713
+ getLocationFromIP,
714
+ buildRedirectUrl,
715
+ detectDevice,
716
+ } from '@linkforty/core/utils';
717
+
718
+ generateShortCode(8);
719
+ // → "aB3kX9mQ" (nanoid-based, configurable length)
720
+
721
+ parseUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...');
722
+ // → { deviceType: "mobile", platform: "iOS", platformVersion: "17.0", browser: "Safari" }
723
+
724
+ getLocationFromIP('8.8.8.8');
725
+ // → { countryCode: "US", countryName: "United States", city: "Mountain View", ... }
726
+
727
+ detectDevice('Mozilla/5.0 (iPhone...)');
728
+ // → "ios"
729
+
730
+ buildRedirectUrl('https://example.com', { source: 'email', medium: 'campaign' });
731
+ // → "https://example.com?utm_source=email&utm_medium=campaign"
732
+ ```
733
+
734
+ ## Error Responses
735
+
736
+ All errors follow this format:
737
+
738
+ ```json
739
+ {
740
+ "error": "Error message",
741
+ "statusCode": 400
742
+ }
743
+ ```
744
+
745
+ | Status | Meaning |
746
+ |--------|---------|
747
+ | 400 | Validation error (bad request body) |
748
+ | 404 | Link/resource not found, or targeting rules excluded user |
749
+ | 500 | Internal server error |
750
+
751
+ ## Real-Time Events
752
+
753
+ Core emits click events via an internal event emitter and optional WebSocket:
754
+
755
+ ```typescript
756
+ import { subscribeToClickEvents } from '@linkforty/core';
757
+
758
+ const unsubscribe = subscribeToClickEvents((event) => {
759
+ console.log('Click:', event.shortCode, event.deviceType, event.country);
760
+ });
761
+
762
+ // WebSocket endpoint: ws://localhost:3000/api/debug/live
763
+ ```