s.plings.io Director Service - Implementation Guide

Created: Tue 29 Jul 2025 07:42:15 CEST
Document Version: 1.0 - Initial comprehensive director system specification
Security Classification: Critical Technical Documentation
Target Audience: System Architects, Full-stack Developers, Product Managers, DevOps Engineers
Author: Paul WisΓ©n

STATUS: πŸ”„ PROCESSING - This comprehensive document requires breakdown and integration into existing documentation structure. See analysis below.

Overview

The s.plings.io service is a lightweight, high-performance edge server dedicated to handling QR code and identifier scanning for the Plings ecosystem. It serves as the primary entry point for all physical-to-digital interactions.

Core Purpose

Architecture Decision

Separate Vercel Project selected for:

Why s.plings.io?

The Director provides four critical functions that would be impossible with direct QR β†’ main app routing:

  1. Director Function 🎯
    • Routes to different services based on object state
    • Enables A/B testing without changing physical QR codes
    • Handles service discovery and geographic routing
  2. Logger Function πŸ“Š
    • Centralized scan analytics across all services
    • Critical for Lost & Found functionality (GPS tracking)
    • Works even if destination service is down
  3. Firewall Function πŸ›‘οΈ
    • Rate limiting and DDoS protection
    • Validates parameters before they hit main services
    • Blocks malicious scans at the edge
  4. Freedom to Evolve πŸš€
    • Add new services without reprinting QR codes
    • Change routing logic instantly
    • Adapt to new technologies

The Permanent QR Code Problem: Physical QR codes cannot be changed. With millions of stickers in circulation, we need the ability to evolve services without reprinting. The Director layer provides this flexibility.

Repository Structure

Repository Name: Plings-Director
URL: https://github.com/[org]/Plings-Director
Documentation: All documentation remains in Plings-Lovable-Frontend/docs/
Code Only: Repository contains only implementation code, no docs

Directory Structure

Plings-Director/
β”œβ”€β”€ api/
β”‚   └── index.ts          # Main route handler (edge function)
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ verification.ts   # Identifier verification logic
β”‚   β”œβ”€β”€ routing.ts        # Routing decision logic
β”‚   β”œβ”€β”€ logging.ts        # Scan event logging
β”‚   β”œβ”€β”€ security.ts       # Rate limiting & validation
β”‚   β”œβ”€β”€ registry.ts       # Manufacturer key registry
β”‚   └── crypto.ts         # Cryptographic utilities
β”œβ”€β”€ types/
β”‚   └── index.ts          # TypeScript type definitions
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ verification.test.ts
β”‚   β”œβ”€β”€ routing.test.ts
β”‚   └── integration.test.ts
β”œβ”€β”€ .env.example          # Example environment variables
β”œβ”€β”€ .gitignore
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ vercel.json           # Vercel configuration
└── README.md             # Basic setup instructions only

Service Specification

Technical Implementation

Core Architecture Philosophy

The Director is fundamentally a smart router - a collection of conditional logic that will evolve and expand over time. Its primary purpose is to make intelligent routing decisions based on multiple factors, and this logic will become more sophisticated as the Plings ecosystem grows.

Edge Function Structure

// /api/index.ts - Main entry point
import { VercelRequest, VercelResponse } from '@vercel/node';

export const config = {
  runtime: 'edge',
  regions: ['iad1', 'sfo1', 'sin1', 'syd1', 'hnd1', 'fra1'] // Global deployment
};

export default async function handler(req: VercelRequest) {
  const start = Date.now();
  
  try {
    // 1. Extract and validate parameters
    const params = extractParams(req.url);
    if (!validateParams(params)) {
      return redirectToError('invalid-params');
    }
    
    // 2. Security checks (rate limiting, etc.)
    const securityCheck = await performSecurityChecks(req, params);
    if (!securityCheck.passed) {
      return redirectToError(securityCheck.reason, securityCheck.status);
    }
    
    // 3. Verify identifier
    const verification = await verifyIdentifier(params);
    if (!verification.isValid) {
      await logFailedVerification(params, verification.reason);
      return redirectToError('invalid-identifier');
    }
    
    // 4. Log scan event (async, don't await)
    logScanEvent({
      ...params,
      ...verification,
      timestamp: Date.now(),
      headers: extractHeaders(req)
    }).catch(console.error);
    
    // 5. Resolve object (if exists)
    const resolution = await resolveObject(params.i);
    
    // 6. ROUTING LOGIC - THE HEART OF THE DIRECTOR
    const targetUrl = await determineRoute(params, verification, resolution, req);
    
    // 7. Redirect with enriched parameters
    return Response.redirect(targetUrl, 302);
    
  } catch (error) {
    console.error('Director error:', error);
    return redirectToError('system-error');
  } finally {
    console.log(`Scan handled in ${Date.now() - start}ms`);
  }
}

Parameter Handling

interface IncomingParams {
  t: 'q' | 'n' | 'r';      // Tag type
  i: string;               // Instance key (48 chars)
  p: string;               // HD wallet path
  cp?: string;             // Class pointer (11 chars)
  // Future: tx, svc, price, dur for direct commerce
}

interface OutgoingParams {
  oid?: string;            // Object UUID (when exists)
  ikey: string;            // Original instance key
  path: string;            // Base58 HD path
  class?: string;          // Class UUID (when resolved)
  cptr?: string;           // Class pointer
  src: 'scan';             // Source tracking
  loc?: string;            // GPS (if available)
  ts?: number;             // Timestamp
}

function enrichParameters(
  incoming: IncomingParams, 
  verification: VerificationResult,
  resolution: ObjectResolution
): URLSearchParams {
  const params = new URLSearchParams({
    ikey: incoming.i,
    path: incoming.p,
    src: 'scan'
  });
  
  if (incoming.cp) params.set('cptr', incoming.cp);
  if (resolution.objectId) params.set('oid', resolution.objectId);
  if (resolution.classId) params.set('class', resolution.classId);
  
  return params;
}

Identifier Verification Implementation

Based on the cached public key strategy outlined in the System Overview, here’s the detailed implementation:

// /api/scan/verification.ts
import { generateClassPointer } from './crypto';

interface VerificationResult {
  isValid: boolean;
  identifierType?: 'generic' | 'manufacturer';
  manufacturerId?: number;
  classId?: string;
  reason?: string;
  cachedVerification?: boolean;
}

// Manufacturer Registry - Multiple storage strategies
class ManufacturerRegistryImpl {
  private cache = new Map<number, string>();
  private lastUpdate = 0;
  private readonly TTL = 3600000; // 1 hour
  
  async getPublicKey(manufacturerId: number): Promise<string | null> {
    // Check cache first
    if (this.cache.has(manufacturerId) && Date.now() - this.lastUpdate < this.TTL) {
      return this.cache.get(manufacturerId)!;
    }
    
    // Strategy 1: Edge Config (Vercel's solution for large configs)
    try {
      const edgeConfig = await import('@vercel/edge-config');
      const publicKey = await edgeConfig.get(`manufacturer_${manufacturerId}_pubkey`);
      if (publicKey) {
        this.cache.set(manufacturerId, publicKey as string);
        return publicKey as string;
      }
    } catch (e) {
      console.log('Edge Config not available');
    }
    
    // Strategy 2: KV Store (Vercel KV or Upstash Redis)
    try {
      const kv = await import('@vercel/kv');
      const publicKey = await kv.get(`mfr:pubkey:${manufacturerId}`);
      if (publicKey) {
        this.cache.set(manufacturerId, publicKey as string);
        return publicKey as string;
      }
    } catch (e) {
      console.log('KV store not available');
    }
    
    // Strategy 3: GraphQL API (with aggressive caching)
    try {
      const response = await fetch(process.env.PLINGS_API_ENDPOINT!, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': 'max-age=3600', // 1 hour cache
        },
        body: JSON.stringify({
          query: `
            query GetManufacturerPublicKey($manufacturerId: Int!) {
              manufacturer(id: $manufacturerId) {
                publicKey
              }
            }
          `,
          variables: { manufacturerId }
        })
      });
      
      if (response.ok) {
        const { data } = await response.json();
        if (data?.manufacturer?.publicKey) {
          const publicKey = data.manufacturer.publicKey;
          this.cache.set(manufacturerId, publicKey);
          this.lastUpdate = Date.now();
          return publicKey;
        }
      }
    } catch (e) {
      console.error('Failed to fetch public key from API:', e);
    }
    
    // Strategy 4: Fallback to essential manufacturers only
    const essentialManufacturers: Record<number, string> = {
      1: process.env.PLINGS_PUBLIC_KEY!,  // Always need Plings key
      // Only add top 5-10 manufacturers here
    };
    
    return essentialManufacturers[manufacturerId] || null;
  }
  
  // Batch load public keys on startup
  async preloadEssentialKeys(): Promise<void> {
    const essentialIds = [1, 3, 17, 42]; // Top manufacturers
    
    try {
      // Try to load from Edge Config
      const edgeConfig = await import('@vercel/edge-config');
      const keys = await edgeConfig.getAll();
      
      Object.entries(keys).forEach(([key, value]) => {
        const match = key.match(/^manufacturer_(\d+)_pubkey$/);
        if (match) {
          this.cache.set(parseInt(match[1]), value as string);
        }
      });
      
      this.lastUpdate = Date.now();
    } catch (e) {
      // Fallback to loading essential keys individually
      for (const id of essentialIds) {
        await this.getPublicKey(id);
      }
    }
  }
}

export const ManufacturerRegistry = new ManufacturerRegistryImpl();

// Initialize on cold start
ManufacturerRegistry.preloadEssentialKeys().catch(console.error);

export async function verifyIdentifier(
  params: IncomingParams
): Promise<VerificationResult> {
  const pathParts = params.p.split('.');
  const manufacturerId = parseInt(pathParts[1]);
  
  // Step 1: Get manufacturer public key
  const manufacturerKey = await ManufacturerRegistry.getPublicKey(manufacturerId);
  if (!manufacturerKey) {
    return { 
      isValid: false, 
      reason: 'Unknown manufacturer',
      manufacturerId 
    };
  }
  
  // Step 2: Verify class pointer if provided
  if (params.cp) {
    const pathToPointer = extractPathToPointer(params.p);
    const expectedPointer = generateClassPointer(manufacturerKey, pathToPointer);
    
    if (expectedPointer !== params.cp) {
      return {
        isValid: false,
        reason: 'Invalid class pointer',
        manufacturerId
      };
    }
  }
  
  // Step 3: For full instance verification, call API
  // This is deferred until after routing decision for performance
  return {
    isValid: true,
    identifierType: manufacturerId === 1 ? 'generic' : 'manufacturer',
    manufacturerId,
    cachedVerification: true
  };
}

Scan Event Logging

// /api/scan/logging.ts
interface ScanEvent {
  // Required fields
  timestamp: number;
  instanceKey: string;
  path: string;
  tagType: 'q' | 'n' | 'r';
  
  // Verification results
  isValid: boolean;
  manufacturerId?: number;
  classPointer?: string;
  
  // Context
  userAgent: string;
  ipAddress: string;
  referer?: string;
  
  // Location (if available)
  latitude?: number;
  longitude?: number;
  accuracy?: number;
  
  // Routing decision
  routedTo: string;
  objectFound: boolean;
  authRequired: boolean;
}

export async function logScanEvent(event: ScanEvent): Promise<void> {
  // Use Vercel Edge Config or external service
  // Fire and forget for performance
  
  try {
    // Option 1: Vercel Analytics
    if (process.env.VERCEL_ANALYTICS_ID) {
      await fetch('https://vitals.vercel-insights.com/v1/events', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          eventName: 'scan',
          properties: event
        })
      });
    }
    
    // Option 2: Custom analytics endpoint
    if (process.env.ANALYTICS_ENDPOINT) {
      await fetch(process.env.ANALYTICS_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.ANALYTICS_API_KEY!
        },
        body: JSON.stringify(event)
      });
    }
  } catch (error) {
    // Log but don't throw - analytics shouldn't break routing
    console.error('Failed to log scan event:', error);
  }
}

Routing Logic - The Evolving Brain

// /lib/routing.ts - This will grow significantly over time
export async function determineRoute(
  params: ScanParams,
  verification: VerificationResult,
  resolution: ObjectResolution | null,
  request: Request
): Promise<string> {
  // Build enriched parameters for downstream services
  const outParams = enrichParameters(params, verification, resolution);
  
  // CASE 1: Unknown identifier
  if (!resolution || !resolution.exists) {
    return routeUnknownIdentifier(params, verification, outParams);
  }
  
  // CASE 2: Known object - Complex routing begins here
  const { object } = resolution;
  
  // Priority 1: Lost items always take precedence
  if (object.status?.includes('LOST')) {
    return `https://plings.io/found/${object.id}?${outParams}`;
  }
  
  // Priority 2: Multi-status handling (object can be for sale, rent, AND lend)
  const commerceRoute = determineCommerceRoute(object, params);
  if (commerceRoute) {
    return commerceRoute;
  }
  
  // Priority 3: Context-aware routing (future expansion)
  const contextualRoute = await determineContextualRoute(object, params, request);
  if (contextualRoute) {
    return contextualRoute;
  }
  
  // Priority 4: Visibility and auth requirements
  if (object.visibility === 'private' || resolution.requiresAuth) {
    return routeRequiringAuth(object, outParams);
  }
  
  // Default: Public object view
  return `https://plings.io/o/${object.id}?${outParams}`;
}

// Multi-status commerce routing (will expand significantly)
function determineCommerceRoute(object: ObjectData, params: ScanParams): string | null {
  const statuses = object.status || [];
  
  // Future: Complex logic for multi-status objects
  // For now, simple priority order
  if (statuses.includes('FOR_SALE')) {
    return `https://market.plings.io/item/${object.id}`;
  }
  if (statuses.includes('FOR_RENT')) {
    return `https://rent.plings.io/item/${object.id}`;
  }
  if (statuses.includes('LENDABLE')) {
    return `https://lend.plings.io/item/${object.id}`;
  }
  
  return null;
}

// Context-aware routing (future expansion area)
async function determineContextualRoute(
  object: ObjectData,
  params: ScanParams,
  request: Request
): Promise<string | null> {
  // Future considerations:
  // - Geographic location (scan at specific venue)
  // - Time of day (business hours vs after hours)
  // - Object class (special handling for certain types)
  // - User agent (mobile vs desktop)
  // - Previous scan history
  // - A/B testing groups
  
  // Example future logic:
  // if (object.class === 'TOOL' && isAtHardwareStore(request)) {
  //   return `https://plings.io/compare-prices/${object.id}`;
  // }
  
  return null;
}

The Director as an Evolving Router

The Director is not a static system - it’s a living, growing router that will become more sophisticated over time. Think of it as the β€œbrain” of the Plings scanning system that learns new routing patterns as the ecosystem expands.

Current State: Simple conditional routing based on object status and visibility Future State: Complex multi-factor decision engine considering context, location, time, user history, and business rules

Why This Architecture?

  1. Start Simple: Launch with basic routing rules that work
  2. Measure Everything: Learn from real-world scanning patterns
  3. Iterate Quickly: Add new routing logic without changing QR codes
  4. Stay Flexible: Adapt to new business requirements easily
  5. Scale Intelligently: Add complexity only where it adds value

Example: Multi-Status Object Routing Evolution

// Version 1 (MVP): Simple priority order
if (status.includes('FOR_SALE')) return market;
if (status.includes('FOR_RENT')) return rent;
if (status.includes('LENDABLE')) return lend;

// Version 2: Consider price ratios
const salePrice = object.metadata.salePrice;
const rentPrice = object.metadata.rentPriceDaily;
if (salePrice && rentPrice) {
  const roi = rentPrice * 365 / salePrice;
  if (roi > 0.15) return rent; // Better ROI on renting
  return market;
}

// Version 3: Add location context
if (scanLocation.type === 'TOURIST_AREA') {
  return rent; // Tourists prefer renting
}
if (scanLocation.type === 'RESIDENTIAL') {
  return market; // Residents prefer buying
}

// Version 4: Machine learning model
const prediction = await routingModel.predict({
  object,
  location,
  timeOfDay,
  userAgent,
  historicalConversions
});
return prediction.optimalRoute;

This evolutionary approach ensures the Director can grow with the Plings ecosystem while maintaining simplicity at each stage.

Configuration-Driven Routing

To manage growing complexity, implement configuration-driven routing:

// routing-config.ts - Externalize routing rules
export const ROUTING_RULES = {
  statusPriority: ['LOST', 'EMERGENCY', 'FOR_SALE', 'FOR_RENT', 'LENDABLE'],
  
  contextRules: [
    {
      condition: (obj) => obj.class === 'MEDICAL_DEVICE' && obj.status.includes('EMERGENCY'),
      route: 'https://emergency.plings.io/device/',
      priority: 1
    },
    {
      condition: (obj) => obj.manufacturerId === 17 && obj.status.includes('RECALL'),
      route: 'https://recall.plings.io/ikea/',
      priority: 2
    }
  ],
  
  // A/B test configuration
  experiments: {
    'new-found-flow': {
      enabled: true,
      percentage: 50,
      control: 'https://plings.io/found/',
      variant: 'https://plings.io/found-v2/'
    }
  }
};

Security Implementation

// /api/scan/security.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// Rate limiting configuration
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 requests per minute
  analytics: true,
});

export const rateLimiter = {
  async check(req: Request): Promise<{ allowed: boolean; remaining: number }> {
    const ip = req.headers.get('x-forwarded-for') || 'unknown';
    const { success, remaining } = await ratelimit.limit(ip);
    
    return { allowed: success, remaining };
  }
};

// Input validation
export function validateParams(params: any): params is IncomingParams {
  // Required parameters
  if (!params.t || !params.i || !params.p) return false;
  
  // Type validation
  if (!['q', 'n', 'r'].includes(params.t)) return false;
  
  // Instance key format (48 chars, base58)
  if (!/^[1-9A-HJ-NP-Za-km-z]{44,48}$/.test(params.i)) return false;
  
  // Path format (dot-separated numbers/base58)
  if (!/^[1-9A-HJ-NP-Za-km-z]+(\.[1-9A-HJ-NP-Za-km-z]+)*$/.test(params.p)) return false;
  
  // Verify class pointer format (11 chars, base58)
  if (params.cp && !/^[1-9A-HJ-NP-Za-km-z]{11}$/.test(params.cp)) return false;
  
  return true;
}

// Error handling
export function redirectToError(reason: string, status = 302): Response {
  const errorUrls: Record<string, string> = {
    'invalid-params': 'https://plings.io/error/invalid-scan',
    'rate-limit': 'https://plings.io/error/too-many-requests',
    'invalid-identifier': 'https://plings.io/error/invalid-identifier',
    'system-error': 'https://plings.io/error/system',
  };
  
  return Response.redirect(
    errorUrls[reason] || errorUrls['system-error'],
    status
  );
}

Use Case Implementation Details

Important Context: Public Scanner Use Cases Only

The following use cases specifically cover public QR code scanning via phone camera apps by users who are not logged into Plings. These represent the entry points for new users and the public face of the Plings system.

For authenticated user workflows (object creation, inventory management, spatial relationships, etc.), see the main application documentation. The Director handles these authenticated flows by redirecting to the appropriate service after login.

UC1: Scanning Unknown Generic Tag (Public)

Scenario: Someone scans a generic Plings tag with their phone camera app

Flow:
1. User scans QR code with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier is valid Plings identifier
4. Log scan event (async)
5. Lookup instance key β†’ NOT FOUND
6. Check class pointer β†’ NONE (generic tag)
7. Route to: https://plings.io/welcome?ikey={instanceKey}&path={path}&src=scan

Frontend shows (public welcome page):
- "You've discovered a Plings tag!"
- Brief explanation of Plings system
- "This tag isn't registered to any object yet"
- [Join Plings Free] - Create account to use this tag
- [I Have an Account] - Log in option
- [Learn More] - What is Plings?

Required GraphQL Operations:

# NEW ENDPOINT NEEDED - Check if identifier exists
query ResolveIdentifier($instanceKey: String!, $path: String!) {
  resolveIdentifier(instanceKey: $instanceKey, path: $path) {
    exists
    object {
      id
      name
      visibility
      status
      owner {
        id
        username
      }
    }
    identifierType  # 'generic' | 'manufacturer'
    manufacturerInfo {
      id
      name
      classInfo
    }
  }
}

# EXISTING - For logging scan events
mutation CreateScanEvent($input: ScanEventInput!) {
  createScanEvent(input: $input) {
    id
    timestamp
  }
}

UC2: Scanning Known Public Object

Scenario: Someone scans an object that exists and is marked as publicly visible

Flow:
1. User scans QR code with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier
4. Log scan event (async)
5. Lookup instance key β†’ FOUND
6. Check object visibility β†’ PUBLIC
7. Route to: https://plings.io/o/{objectId}?ikey={instanceKey}&path={path}&src=scan

Frontend shows (public object page):
- Object name and main image
- Basic description (if public)
- "Owned by: [Organization Name]"
- [Found This Item?] - If you found this
- [Join Plings] - To manage your own objects
- NO private information displayed

UC3: Scanning Lost Item (Public)

Scenario: Someone finds a lost item and scans its QR code

Flow:
1. Finder scans QR code with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier
4. Log scan event WITH GPS (if permitted)
5. Lookup instance key β†’ FOUND
6. Check status β†’ Contains 'LOST'
7. Trigger owner notification (async)
8. Route to: https://plings.io/found/{objectId}?ikey={instanceKey}&path={path}&src=scan

Frontend shows (public found page per lost-item-workflow.md):
- "This item has been reported lost!"
- "Thank you for scanning it"
- Item name and photo (if public)
- Finder's fee amount (e.g., "$20 Reward")
- [I Found This Item] - Contact owner button
- Owner contact info (phone shown, email via app)
- "The owner has been notified"
- Instructions for safe return
- [Get Plings App] - Convert finder to user

Note: This aligns with the comprehensive lost item workflow at docs/use-cases/lost-item-workflow.md. The public found page serves as a critical user acquisition channel by introducing finders to the Plings system.

UC4: Scanning Manufacturer Product (Public)

Scenario: Customer scans QR code on a new product in store or at home

Flow:
1. Customer scans product QR with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier with manufacturer key
4. Verify class pointer is valid
5. Log scan event
6. Lookup instance key β†’ NOT FOUND
7. Route to: https://plings.io/claim?ikey={instanceKey}&path={path}&cptr={classPointer}&mfr={manufacturerId}&src=scan

Frontend shows (public product page):
- Manufacturer logo and name
- Product information and image
- "Register for warranty and support"
- [Register This Product] - Requires account
- [Create Account] - New users
- [Log In] - Existing users
- Product specs and info (public)

UC5: Scanning Private/Restricted Object (Public)

Scenario: Someone scans an object marked as private or organization-only

Flow:
1. User scans QR code with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier
4. Log scan event
5. Lookup instance key β†’ FOUND
6. Check visibility β†’ PRIVATE/ORGANIZATION
7. Route to: https://plings.io/restricted?src=scan

Frontend shows (restricted page):
- "This is a private Plings object"
- No object details shown
- [Are You the Owner?] - Log in to view
- [Found This Item?] - Report if found
- [Learn About Plings] - General info

UC6: Scanning Object for Sale (Public)

Scenario: Potential buyer scans QR on item for sale

Flow:
1. Buyer scans QR code with phone camera
2. Phone opens browser to s.plings.io
3. Director verifies identifier
4. Log scan event
5. Lookup instance key β†’ FOUND
6. Check status β†’ Contains 'FOR_SALE'
7. Route to: https://market.plings.io/item?oid={objectId}&ikey={instanceKey}&path={path}&src=scan

Market.plings.io shows (public listing):
- Item photos and description
- Price and seller info (organization)
- [Contact Seller] - Requires account
- [Create Account to Buy] - New users
- Public questions/answers
- Similar items for sale

What Happens After Login?

Once a user creates an account or logs in, they gain access to the full Plings ecosystem:

The Director automatically handles authenticated users differently, routing them to appropriate management interfaces rather than these public pages.

Environment Configuration

# Vercel Environment Variables for s.plings.io
# Keep under 50 variables limit!

# Essential Configuration
PLINGS_PUBLIC_KEY=<base58_encoded_public_key>  # Always required
PLINGS_API_ENDPOINT=https://api.plings.io/graphql

# Storage Solutions (choose one primary method)
# Option A: Vercel Edge Config
EDGE_CONFIG=https://edge-config.vercel.com/<token>

# Option B: KV Store
KV_REST_API_URL=https://<instance>.vercel.kv.io
KV_REST_API_TOKEN=<token>

# Option C: Upstash Redis (shared with rate limiting)
UPSTASH_REDIS_REST_URL=https://<instance>.upstash.io
UPSTASH_REDIS_REST_TOKEN=<token>

# Analytics & Monitoring
ANALYTICS_ENDPOINT=https://analytics.plings.io/v1/events
ANALYTICS_API_KEY=<secure_api_key>
SENTRY_DSN=<sentry_dsn_for_edge_functions>

# Feature Flags
ENABLE_INSTANCE_VERIFICATION=true
ENABLE_GPS_LOGGING=true
ENABLE_DEEP_LINKING=true

# Cache Configuration
EDGE_CACHE_TTL=300  # 5 minutes for object data
MANUFACTURER_CACHE_TTL=3600  # 1 hour for public keys

# Logging
LOG_LEVEL=info

Manufacturer Public Key Storage Solutions

// Store all manufacturer keys in Edge Config
// No environment variable limit, optimized for edge reading
{
  "manufacturer_1_pubkey": "Base58PublicKeyForPlings",
  "manufacturer_3_pubkey": "Base58PublicKeyForCocaCola",
  "manufacturer_17_pubkey": "Base58PublicKeyForIKEA",
  // ... hundreds more
}

Option B: Vercel KV Store

# Store keys in KV store via CLI or API
vercel kv set mfr:pubkey:1 "Base58PublicKeyForPlings"
vercel kv set mfr:pubkey:3 "Base58PublicKeyForCocaCola"
vercel kv set mfr:pubkey:17 "Base58PublicKeyForIKEA"

Option C: API Endpoint with CDN Caching

// Serve from API with aggressive caching
// /api/manufacturers/registry.json
{
  "manufacturers": {
    "1": { "name": "Plings", "publicKey": "..." },
    "3": { "name": "Coca-Cola", "publicKey": "..." },
    "17": { "name": "IKEA", "publicKey": "..." }
  }
}

Deployment Configuration

Vercel Configuration (vercel.json)

{
  "name": "plings-director",
  "alias": ["s.plings.io"],
  "regions": ["iad1", "sfo1", "sin1", "syd1", "hnd1", "fra1"],
  "functions": {
    "api/index.ts": {
      "runtime": "edge",
      "maxDuration": 5
    }
  },
  "rewrites": [
    {
      "source": "/",
      "destination": "/api/index"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ]
}

Monitoring & Observability

Key Metrics to Track

// Custom metrics for monitoring
interface DirectorMetrics {
  // Performance
  responseTime: Histogram;         // Target: <100ms p95
  verificationTime: Histogram;     // Target: <20ms
  routingTime: Histogram;         // Target: <10ms
  
  // Volume
  scanVolume: Counter;            // Total scans
  scanByType: Counter;            // QR vs NFC vs RFID
  scanByManufacturer: Counter;    // Per manufacturer
  scanByService: Counter;         // Where routed
  
  // Errors
  verificationFailures: Counter;   // Invalid identifiers
  routingErrors: Counter;         // Failed routing
  rateLimitHits: Counter;         // Rate limit rejections
  
  // Cache Performance
  cacheHitRate: Gauge;           // Object cache hits
  manufacturerCacheHits: Gauge;   // Public key cache
}

// Monitoring implementation
export function recordMetrics(event: string, value: number, tags?: Record<string, string>) {
  // Send to monitoring service
  if (process.env.MONITORING_ENABLED) {
    // DataDog, New Relic, or custom solution
  }
}

Health Check Endpoint

// /api/health
export default async function healthCheck(req: Request) {
  const checks = {
    edge: 'ok',
    redis: await checkRedis(),
    api: await checkAPI(),
    cache: await checkCache(),
  };
  
  const allHealthy = Object.values(checks).every(v => v === 'ok');
  
  return new Response(JSON.stringify({
    status: allHealthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    checks,
    region: process.env.VERCEL_REGION,
  }), {
    status: allHealthy ? 200 : 503,
    headers: { 'Content-Type': 'application/json' }
  });
}

Testing Strategy

Unit Tests

// __tests__/verification.test.ts
describe('Identifier Verification', () => {
  it('should verify valid class pointer', async () => {
    const params = {
      t: 'q',
      i: '4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo',
      p: '2.17.3.2.5.100.5000',
      cp: '4K7mX9abDcE'
    };
    
    const result = await verifyIdentifier(params);
    expect(result.isValid).toBe(true);
    expect(result.manufacturerId).toBe(17);
  });
  
  it('should reject invalid class pointer', async () => {
    const params = {
      t: 'q',
      i: '4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo',
      p: '2.17.3.2.5.100.5000',
      cp: 'INVALID123'
    };
    
    const result = await verifyIdentifier(params);
    expect(result.isValid).toBe(false);
    expect(result.reason).toBe('Invalid class pointer');
  });
});

Integration Tests

// __tests__/routing.integration.test.ts
describe('Director Routing Integration', () => {
  it('should route FOR_SALE objects to marketplace', async () => {
    // Mock object resolution
    mockResolveObject({
      exists: true,
      object: {
        id: 'obj-123',
        status: ['FOR_SALE'],
        visibility: 'public'
      }
    });
    
    const response = await fetch('https://s.plings.io?t=q&i=TEST&p=2.1.1.1.1');
    
    expect(response.status).toBe(302);
    expect(response.headers.get('Location')).toContain('market.plings.io');
  });
});

Frontend Requirements

New Routes and Pages

1. /o/{objectId} - Public Object View

Purpose: Display public information about an object to anyone who scans it URL Structure: /o/{objectId} (using directory structure for cleaner URLs and better SEO)

Why directory instead of page?

Page Requirements:

interface PublicObjectPageProps {
  // URL params
  objectId: string;
  
  // Query params from scan
  ikey?: string;      // Instance key (for verification)
  path?: string;      // HD path
  src?: string;       // Source (always 'scan' from Director)
}

// Display requirements
- Object name and main image
- Public description
- Owner organization (not personal info)
- Object status badges (if public)
- [Contact Owner] button (if enabled)
- [Report Found] if status includes 'lost'
- [View in App] call-to-action for non-users
- Share buttons for social media

2. /welcome - New User Onboarding

Purpose: Educate potential new users about Plings when they scan an unregistered tag URL: /welcome?ikey={instanceKey}&path={path}&src=scan

Page Requirements:

3. /found/{objectId} - Lost Item Found Flow

Purpose: Handle the sensitive flow when someone finds a lost item URL: /found/{objectId}?ikey={instanceKey}&path={path}&src=scan

Page Requirements (from lost-item-workflow.md):

πŸ” Item Found: [Item Name]

πŸ“Š Status: LOST - Reward Available
πŸ’° Finder's Fee: $20.00

πŸ‘€ Owner Contact:
   πŸ“ž Phone: (555) 123-4567
   πŸ“§ Email: [Contact through app]

πŸ“ Last Seen: [Location]
⏰ Lost Since: [Time ago]

πŸ’‘ First time using Plings?
   This is a lost & found system that helps people
   recover their belongings. You can help by contacting
   the owner and earn a reward!

πŸ“± Get Plings App:
   β€’ Track your own items
   β€’ Earn rewards for helping others
   β€’ Join the community

[πŸ“ž Call Owner] [πŸ“± Get App] [❌ Close]

Key Features:

4. /claim - Manufacturer Product Registration

Purpose: Register/claim a manufacturer’s product URL: /claim?ikey={instanceKey}&path={path}&cptr={classPointer}&mfr={manufacturerId}&src=scan

Page Requirements:

5. /scan-error - Error Pages

Purpose: User-friendly error messages for scan issues URLs:

Page Requirements:

Authentication Flow Pages

/login with Scan Context

Enhanced Requirements:

// When redirected from scan requiring auth
interface LoginPageScanContext {
  return: 'scan';
  state: string;  // Encoded scan parameters
}

// After successful login:
1. Decode state parameters
2. Call GraphQL to re-verify access
3. Redirect to appropriate view based on permissions

Mobile Optimization Requirements

Since most scans come from mobile devices:

  1. Touch-Optimized: Large buttons, easy tap targets
  2. Fast Loading: Minimize JavaScript, optimize images
  3. Offline Capable: Show cached data when possible
  4. Camera-First: Quick return to camera for multiple scans
  5. Progressive Enhancement: Work without JavaScript for basic viewing

URL Parameter Handling

All pages receiving Director parameters should:

  1. Validate Parameters: Ensure they match expected format
  2. Track Source: Record that visit came from scan
  3. Preserve Context: Maintain parameters through auth flows
  4. Handle Missing Object: Graceful fallback if object deleted

Analytics Requirements

Track on all Director-routed pages:

Backend Requirements

New GraphQL Endpoints Needed

The following GraphQL operations need to be implemented in the backend to support the Director service:

# 1. Identifier Resolution - Check if identifier exists and get basic info
query ResolveIdentifier($instanceKey: String!, $path: String!) {
  resolveIdentifier(instanceKey: $instanceKey, path: $path) {
    exists: Boolean!
    identifierType: String!  # 'generic' | 'manufacturer'
    
    # If object exists
    object {
      id: ID!
      name: String!
      visibility: String!  # 'public' | 'private' | 'organization'
      status: [String!]    # Array of status keys
      requiresAuth: Boolean!
      owner {
        id: ID!
        username: String!
        organizationName: String
      }
    }
    
    # For manufacturer tags
    manufacturerInfo {
      id: Int!
      name: String!
      classId: String
      className: String
      classDescription: String
    }
  }
}

# 2. Manufacturer Public Key Query
query GetManufacturerPublicKey($manufacturerId: Int!) {
  manufacturer(id: $manufacturerId) {
    id: Int!
    name: String!
    publicKey: String!  # Base58 encoded public key
  }
}

# 3. Scan Event Logging
mutation CreateScanEvent($input: ScanEventInput!) {
  createScanEvent(input: $input) {
    id: ID!
    timestamp: DateTime!
    processed: Boolean!
  }
}

input ScanEventInput {
  instanceKey: String!
  path: String!
  tagType: String!  # 'q' | 'n' | 'r'
  classPointer: String
  
  # Context
  userAgent: String
  ipAddress: String
  referer: String
  
  # Location (optional)
  latitude: Float
  longitude: Float
  accuracy: Float
  
  # Resolution results
  objectFound: Boolean!
  routedTo: String!
  verificationResult: String
}

Implementation Priority

  1. Critical (Block Director launch):
    • resolveIdentifier query
    • manufacturer.publicKey field
  2. Important (Can use workarounds initially):
    • createScanEvent mutation (can log locally first)
  3. Nice to have:
    • Batch identifier resolution
    • Scan analytics queries

Troubleshooting Guide

Common Issues

  1. High Latency
    • Check edge function region deployment
    • Verify cache hit rates
    • Monitor API response times
  2. Verification Failures
    • Ensure manufacturer public keys are current
    • Check class pointer generation algorithm
    • Verify path format parsing
  3. Routing Errors
    • Confirm object resolution API is accessible
    • Check service URL configurations
    • Verify parameter enrichment logic
  4. Rate Limiting Issues
    • Review rate limit thresholds
    • Check for bot traffic patterns
    • Consider geographic rate limit variations

Future Enhancements

  1. Semi-Public Key Registry (Phase 2)
    • Implement path registry for campaign-level public keys
    • Enable deeper offline verification
    • Further reduce API calls
  2. WebAssembly Crypto (Phase 3)
    • Port verification algorithms to WASM
    • Enable full offline verification
    • Sub-10ms verification times
  3. Smart Routing (Future)
    • ML-based routing predictions
    • User preference learning
    • Context-aware service selection

API Endpoints

Primary Scanner

GET https://s.plings.io?t={type}&i={instance}&p={path}&cp={classpointer}

Health Check

GET https://s.plings.io/health
Response: { status: "ok", latency: 12 }

Appendix A: Implementation Checklist

Repository Setup

Phase 1: MVP Launch

Phase 2: Enhanced Verification

Phase 3: Advanced Features

Documentation (in Plings-Lovable-Frontend)

External Resources


This implementation guide is stored in Plings-Lovable-Frontend/docs/implementation/ while the actual code implementation resides in the Plings-Director repository. This separation ensures documentation remains centralized while code is independently deployable.