Loading learning content...
While auto-generated codes like a7Xk2B are efficient for the system, they're meaningless to humans. Users—especially enterprise customers running marketing campaigns—want memorable, branded, meaningful short URLs:
short.url/summer-sale instead of short.url/x8Yk3zshort.url/careers instead of short.url/2BnQ9fshort.url/webinar-2024 instead of short.url/kL5mP1Custom aliases (also called vanity URLs) are a premium feature that drives monetization for many URL shorteners. Bitly's paid plans, for example, heavily feature custom link branding.
But custom aliases introduce significant complexity: namespace conflicts, reserved words, abuse prevention, and race conditions in a distributed system. Let's master each challenge.
By the end of this page, you will understand how to validate and secure custom aliases, prevent namespace collisions with auto-generated codes, handle concurrent requests, implement reservation systems, and design custom domain support.
Before implementation, let's fully understand what custom alias support requires:
| Feature | Description | Priority |
|---|---|---|
| User-specified aliases | Users input their desired short code | P0 (Core) |
| Availability check | Real-time check if alias is available | P0 (Core) |
| Alias validation | Enforce format rules (length, characters) | P0 (Core) |
| Reserved word blocking | Prevent system paths and protected terms | P0 (Security) |
| Custom domains | User's own domain (link.company.com) | P1 (Premium) |
| Alias suggestions | Offer alternatives if requested alias is taken | P2 (UX) |
| Alias history | Track previous aliases for a URL | P3 (Nice-to-have) |
Custom aliases must coexist with auto-generated codes in the same namespace. This creates several constraints:
If auto-generated codes are 7-character Base62 strings, and a user requests the custom alias 'abc1234', is that a collision risk? It could be auto-generated in the future. Solutions include: separate namespaces, different lengths, or special characters that auto-generation never uses.
Alias validation is the first line of defense. We must enforce strict rules while providing helpful feedback.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
/** * Custom Alias Validation * * Comprehensive validation with clear, actionable error messages. */ interface ValidationResult { valid: boolean; error?: string; normalizedAlias?: string; suggestions?: string[];} class AliasValidator { // Configuration private readonly MIN_LENGTH = 3; private readonly MAX_LENGTH = 30; private readonly ALLOWED_CHARS = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/; // Reserved words that cannot be used as aliases private readonly RESERVED_WORDS = new Set([ // System paths 'api', 'admin', 'dashboard', 'login', 'logout', 'signup', 'register', 'settings', 'profile', 'account', 'billing', 'help', 'support', 'docs', 'documentation', 'status', 'health', 'metrics', 'analytics', // Common pages 'home', 'index', 'about', 'contact', 'terms', 'privacy', 'legal', 'tos', 'blog', 'news', 'faq', 'pricing', 'enterprise', // Infrastructure 'www', 'mail', 'smtp', 'ftp', 'cdn', 'static', 'assets', 'images', 'css', 'js', 'fonts', 'media', 'download', 'downloads', // Potential abuse 'admin', 'administrator', 'root', 'null', 'undefined', 'test', 'example', 'demo', 'sample', 'temp', 'tmp', ]); // Patterns that indicate potential abuse private readonly BLOCKED_PATTERNS = [ /^[0-9]+$/, // Pure numbers (conflict with auto-gen) /password|passwd|secret/i, // Security-related /admin|root|sudo/i, // Privilege-related /\.\.|~|\/|\\/, // Path traversal attempts /^[_-]/, // Can't start with special char /[_-]$/, // Can't end with special char /[-_]{2,}/, // No consecutive special chars ]; validate(alias: string): ValidationResult { // Normalize (lowercase, trim) const normalized = alias.toLowerCase().trim(); // Length check if (normalized.length < this.MIN_LENGTH) { return { valid: false, error: `Alias must be at least ${this.MIN_LENGTH} characters`, }; } if (normalized.length > this.MAX_LENGTH) { return { valid: false, error: `Alias cannot exceed ${this.MAX_LENGTH} characters`, }; } // Character format check if (!this.ALLOWED_CHARS.test(alias)) { return { valid: false, error: 'Alias can only contain letters, numbers, hyphens, and underscores, and must start with a letter or number', }; } // Reserved word check if (this.RESERVED_WORDS.has(normalized)) { return { valid: false, error: 'This alias is reserved and cannot be used', suggestions: this.generateSuggestions(normalized), }; } // Blocked pattern check for (const pattern of this.BLOCKED_PATTERNS) { if (pattern.test(normalized)) { return { valid: false, error: 'This alias format is not allowed', }; } } return { valid: true, normalizedAlias: normalized, }; } private generateSuggestions(base: string): string[] { // Offer alternatives when requested alias is unavailable const year = new Date().getFullYear(); return [ `${base}-${year}`, `${base}-link`, `my-${base}`, `${base}-1`, `go-${base}`, ]; }}Major brands may want to reserve their trademarks even before using them. Consider a 'trademark reservation' feature where verified brand owners can block their trademarks from being used by others, preventing brand confusion and phishing.
The most critical challenge is preventing collisions between custom aliases and auto-generated codes. Three main strategies exist:
1234567891011121314151617181920
/** * Length-Based Namespace Separation * * Auto-generated codes are EXACTLY 7 characters. * Custom aliases are anything EXCEPT 7 characters. */ const AUTO_GEN_LENGTH = 7; function isAutoGenerated(code: string): boolean { return code.length === AUTO_GEN_LENGTH;} function validateCustomLength(alias: string): boolean { // Custom aliases: 3-6 chars OR 8-30 chars (never 7) return alias.length >= 3 && alias.length <= 30 && alias.length !== AUTO_GEN_LENGTH;} // Pros: Simple, no runtime overhead// Cons: Limits custom alias flexibility (can't use 7-char aliases)1234567891011121314151617181920212223242526
/** * Prefix-Based Namespace Separation * * Auto-generated codes start with a character never used in custom aliases. * For example: auto-gen starts with '0', custom never starts with '0'. */ const AUTO_GEN_PREFIX = '0'; // Auto-generated codes: 0xxxxxx function generateAutoCode(): string { const id = generateSnowflakeId(); const base62 = encodeBase62(id); return AUTO_GEN_PREFIX + base62.slice(0, 6); // Total: 7 chars} function validateCustomAlias(alias: string): boolean { // Custom aliases cannot start with '0' return !alias.startsWith('0') && alias.length >= 3;} // URL examples:// Auto-generated: short.url/0a7Xk2B// Custom: short.url/summer-sale // Pros: Any length for custom, clear visual distinction// Cons: Auto-generated URLs slightly less attractive1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Unified Namespace with Collision Check * * All codes (auto and custom) share the same namespace. * Every creation checks for existing codes. */ async function createShortUrl( longUrl: string, customAlias?: string): Promise<ShortUrl> { if (customAlias) { // Custom alias path const validation = validator.validate(customAlias); if (!validation.valid) { throw new ValidationError(validation.error!); } // Check availability const exists = await checkExists(validation.normalizedAlias!); if (exists) { throw new ConflictError('This alias is already taken'); } return await insertUrl(validation.normalizedAlias!, longUrl); } else { // Auto-generated path with collision handling let attempts = 0; while (attempts < 5) { const code = generateAutoCode(); try { return await insertUrl(code, longUrl); } catch (error) { if (isUniqueViolation(error)) { attempts++; continue; // Collision with existing (rare but possible) } throw error; } } throw new Error('Failed to generate unique code after 5 attempts'); }} // Pros: Maximum flexibility, no artificial constraints// Cons: More complex, theoretical collision between auto and customPrefix-based separation is the cleanest solution. It provides guaranteed zero collisions with minimal constraints on custom aliases. The visual distinction (codes starting with '0' are auto-generated) also aids debugging and support.
What happens when two users simultaneously request the same custom alias? In a distributed system, this race condition must be handled correctly.
123456789101112131415161718
Race Condition: Two Users Request Same Alias============================================= Time | User A | User B--------|---------------------------|---------------------------T+0ms | Check: "summer-sale" |T+5ms | Result: available | Check: "summer-sale"T+10ms | | Result: availableT+15ms | Insert "summer-sale" | Insert "summer-sale"T+20ms | Success! (maybe) | Success or Failure? Problem: Both users saw "available", both try to insert.Without proper handling, results are undefined. Required outcomes:- Exactly one user succeeds- The other gets clear error message- No partial or corrupt state1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
/** * Handling Concurrent Alias Requests * * Use database unique constraint as the source of truth. * The "check then insert" pattern is fundamentally broken; * use "insert and handle failure" instead. */ // Database Schema (PostgreSQL)const schema = `CREATE TABLE urls ( id BIGSERIAL PRIMARY KEY, short_code VARCHAR(30) NOT NULL, short_code_lower VARCHAR(30) NOT NULL, -- Lowercase for case-insensitive uniqueness long_url TEXT NOT NULL, user_id BIGINT, created_at TIMESTAMPTZ DEFAULT NOW(), -- Unique constraint is the source of truth CONSTRAINT unique_short_code UNIQUE (short_code_lower)); CREATE INDEX idx_urls_short_code ON urls(short_code_lower);`; async function createWithCustomAlias( longUrl: string, customAlias: string, userId: string): Promise<ShortUrl> { // Validation (format check only, not availability) const validation = validator.validate(customAlias); if (!validation.valid) { throw new ValidationError(validation.error!); } const normalizedAlias = validation.normalizedAlias!; try { // Attempt insert directly - let DB enforce uniqueness const result = await db.query(` INSERT INTO urls (short_code, short_code_lower, long_url, user_id) VALUES ($1, $2, $3, $4) RETURNING id, short_code `, [customAlias, normalizedAlias, longUrl, userId]); return { shortCode: result.rows[0].short_code, longUrl, shortUrl: `https://short.url/${result.rows[0].short_code}`, }; } catch (error) { if (error.code === '23505') { // PostgreSQL unique violation // Check if it's the user's own duplicate submission const existing = await db.query(` SELECT user_id, long_url FROM urls WHERE short_code_lower = $1 `, [normalizedAlias]); if (existing.rows[0]?.user_id === userId && existing.rows[0]?.long_url === longUrl) { // Idempotent: same user, same URL - return existing return { shortCode: customAlias, longUrl, shortUrl: `https://short.url/${customAlias}`, }; } // Different user or different URL - genuine conflict throw new ConflictError('This alias is already taken', { suggestions: validator.generateSuggestions(normalizedAlias), }); } throw error; }}The pattern 'check if available, then insert' is a race condition waiting to happen. Always insert first and handle the unique constraint violation. This applies to any distributed system with uniqueness requirements—usernames, email addresses, etc.
Users want immediate feedback on alias availability as they type. This 'instant availability check' is UX-critical but must be implemented carefully.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
/** * Alias Availability Check * * Provides instant feedback to users with caveats about race conditions. */ // GET /api/v1/aliases/check?alias=summer-saleinterface AvailabilityResponse { alias: string; available: boolean; reason?: 'taken' | 'reserved' | 'invalid_format' | 'too_short' | 'too_long'; message?: string; suggestions?: string[];} async function checkAvailability(alias: string): Promise<AvailabilityResponse> { const normalized = alias.toLowerCase().trim(); // 1. Format validation first (no DB query needed) const validation = validator.validate(alias); if (!validation.valid) { return { alias, available: false, reason: getReasonFromError(validation.error!), message: validation.error, }; } // 2. Check reserved words (no DB query needed) if (reservedWords.has(normalized)) { return { alias, available: false, reason: 'reserved', message: 'This alias is reserved and cannot be used', suggestions: generateSuggestions(normalized), }; } // 3. Check database for existing usage const existing = await cache.get(`alias:${normalized}`) ?? await db.query( 'SELECT 1 FROM urls WHERE short_code_lower = $1 LIMIT 1', [normalized] ); if (existing) { return { alias, available: false, reason: 'taken', message: 'This alias is already in use', suggestions: generateSuggestions(normalized), }; } // 4. Available (but warn about race condition) return { alias, available: true, message: 'Available! Note: availability is not reserved until creation', };} // Debounced client-side implementationclass AliasChecker { private debounceMs = 300; private controller: AbortController | null = null; async check(alias: string): Promise<AvailabilityResponse> { // Cancel previous request if (this.controller) { this.controller.abort(); } this.controller = new AbortController(); // Wait for debounce period await new Promise(r => setTimeout(r, this.debounceMs)); const response = await fetch(`/api/v1/aliases/check?alias=${encodeURIComponent(alias)}`, { signal: this.controller.signal, }); return response.json(); }}Make clear in the UI that checking availability does not reserve the alias. Between checking and submitting, someone else might take it. Some systems offer temporary reservations (hold for 5 minutes while user completes form), but this adds significant complexity.
Enterprise customers want URLs on their own domains: links.company.com/careers instead of short.url/careers. This is a major premium feature.
12345678910111213141516171819202122232425262728293031323334353637
Custom Domain Flow================== Setup Phase:1. User adds domain: links.company.com2. User sets DNS: CNAME links.company.com → shortener.example.com3. System validates DNS configuration4. System provisions SSL certificate (Let's Encrypt)5. Domain becomes active Request Flow: ┌─────────────────┐ ┌──────────────────────┐│ Browser │────▶│ links.company.com │ (DNS: CNAME)└─────────────────┘ └───────────┬──────────┘ │ ▼ ┌───────────────────────┐ │ Our Load Balancer │ │ (Receives request) │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ Domain Identification │ │ • Extract Host header │ │ • Lookup domain owner │ │ • Verify domain active│ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ URL Resolution │ │ • Match domain+path │ │ • Lookup short code │ │ • Return redirect │ └───────────────────────┘123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
/** * Custom Domain Management */ interface CustomDomain { id: string; domain: string; // links.company.com userId: string; // Owner status: 'pending' | 'validating' | 'active' | 'error'; sslStatus: 'pending' | 'issued' | 'expired' | 'error'; createdAt: Date; validatedAt?: Date;} class CustomDomainManager { /** * Add a custom domain for a user. */ async addDomain(userId: string, domain: string): Promise<CustomDomain> { // Validate domain format if (!this.isValidDomain(domain)) { throw new ValidationError('Invalid domain format'); } // Check not already registered const existing = await this.getDomain(domain); if (existing) { throw new ConflictError('Domain already registered'); } // Create pending domain record const customDomain = await db.insert('custom_domains', { domain, userId, status: 'pending', sslStatus: 'pending', }); // Start async validation this.queueValidation(customDomain.id); return customDomain; } /** * Validate domain DNS is correctly configured. */ async validateDns(domainId: string): Promise<boolean> { const domain = await this.getDomainById(domainId); // Check CNAME record points to our service const records = await dns.resolveCname(domain.domain); const validTarget = 'shortener.example.com'; if (!records.includes(validTarget)) { await this.updateStatus(domainId, 'error', 'CNAME record not found. Add: ${domain.domain} CNAME ${validTarget}'); return false; } // Provision SSL certificate await this.provisionSsl(domain); await this.updateStatus(domainId, 'active'); return true; } /** * Provision SSL certificate via Let's Encrypt. */ async provisionSsl(domain: CustomDomain): Promise<void> { // Using certbot or acme.js for Let's Encrypt const cert = await acme.getCertificate({ domain: domain.domain, webroot: '/var/www/acme-challenge', }); // Store certificate await this.storeCertificate(domain.domain, cert); await this.updateSslStatus(domain.id, 'issued'); }} // Database schema for custom domainsconst schema = `CREATE TABLE custom_domains ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain VARCHAR(255) UNIQUE NOT NULL, user_id BIGINT NOT NULL REFERENCES users(id), status VARCHAR(20) DEFAULT 'pending', ssl_status VARCHAR(20) DEFAULT 'pending', ssl_expires TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), validated_at TIMESTAMPTZ); -- URLs table now includes domainCREATE TABLE urls ( id BIGSERIAL PRIMARY KEY, domain_id UUID REFERENCES custom_domains(id), -- NULL = default domain short_code VARCHAR(30) NOT NULL, long_url TEXT NOT NULL, -- Unique within domain (same code can exist on different domains) CONSTRAINT unique_domain_code UNIQUE (domain_id, short_code));`;With custom domains, the same short code can exist on multiple domains: short.url/careers and links.company.com/careers can point to different URLs. The database uniqueness constraint becomes (domain, short_code) instead of just (short_code).
Custom aliases introduce unique abuse vectors. Attackers may try to squat valuable names, create phishing links, or exhaust the namespace.
| Abuse Type | Description | Mitigation |
|---|---|---|
| Name Squatting | Register 'google', 'apple', 'tesla' to resell or misuse | Trademark blocklist, premium pricing, verification |
| Namespace Exhaustion | Register thousands of common words to block others | Rate limits, per-user quotas, abuse detection |
| Brand Impersonation | Create 'googl-support' for phishing | Similarity detection, brand protection program |
| Offensive Content | Register vulgar/offensive aliases | Content filtering, human review for edge cases |
| Typosquatting | Register 'gogle', 'gooogle' variations | Levenshtein distance check against protected terms |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
/** * Abuse Prevention for Custom Aliases */ class AbusePreventor { // Known brand names that require verification private protectedBrands = new Set([ 'google', 'apple', 'microsoft', 'amazon', 'facebook', 'meta', 'netflix', 'tesla', 'uber', 'airbnb', 'twitter', 'instagram', // ... loaded from database ]); // Offensive word list private offensivePatterns: RegExp[]; async checkAlias(alias: string, userId: string): Promise<AbuseCheckResult> { const normalized = alias.toLowerCase(); const checks: AbuseCheck[] = []; // 1. Protected brand check const brandMatch = this.findBrandMatch(normalized); if (brandMatch) { checks.push({ type: 'brand_protection', severity: 'block', message: `"${brandMatch}" is a protected brand name`, }); } // 2. Typosquatting detection const typosquatVictim = this.checkTyposquatting(normalized); if (typosquatVictim) { checks.push({ type: 'typosquatting', severity: 'block', message: `Too similar to protected name "${typosquatVictim}"`, }); } // 3. Offensive content filter if (this.containsOffensive(normalized)) { checks.push({ type: 'offensive', severity: 'block', message: 'This alias contains prohibited content', }); } // 4. User quota check const userAliasCount = await this.getUserAliasCount(userId); const userLimit = await this.getUserLimit(userId); if (userAliasCount >= userLimit) { checks.push({ type: 'quota', severity: 'block', message: `Custom alias limit reached (${userLimit}). Upgrade for more.`, }); } // 5. Rate limiting const recentCreations = await this.getRecentCreationCount(userId, '1 hour'); if (recentCreations > 10) { checks.push({ type: 'rate_limit', severity: 'delay', message: 'Too many aliases created recently. Please wait.', }); } return { allowed: checks.filter(c => c.severity === 'block').length === 0, checks, }; } /** * Check if alias is too similar to protected brand (Levenshtein distance) */ private checkTyposquatting(alias: string): string | null { for (const brand of this.protectedBrands) { const distance = levenshteinDistance(alias, brand); const threshold = Math.max(2, Math.floor(brand.length * 0.3)); if (distance <= threshold && distance > 0) { return brand; } } return null; }} function levenshteinDistance(a: string, b: string): number { const matrix: number[][] = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b[i-1] === a[j-1]) { matrix[i][j] = matrix[i-1][j-1]; } else { matrix[i][j] = Math.min( matrix[i-1][j-1] + 1, // substitution matrix[i][j-1] + 1, // insertion matrix[i-1][j] + 1 // deletion ); } } } return matrix[b.length][a.length];}Short URLs are commonly used in phishing attacks because they obscure the true destination. Implement destination URL scanning (Google Safe Browsing API, VirusTotal), rate limit new URL creations, and flag suspicious patterns (newly registered domains, financial site look-alikes).
We've thoroughly covered custom alias implementation. Let's consolidate the key decisions:
| Aspect | Recommended Approach | Rationale |
|---|---|---|
| Namespace Separation | Prefix-based (auto starts with '0') | Zero collision risk, minimal constraints |
| Collision Handling | Database unique constraint | Single source of truth, handles races |
| Case Sensitivity | Case-preserving, case-insensitive unique | Show user's case, match any case |
| Availability Check | Check format first, DB second, debounced | Fast feedback, efficient queries |
| Custom Domains | Domain-scoped uniqueness | Same code on different domains OK |
| Abuse Prevention | Multi-layer: blocklist + similarity + quotas | Defense in depth |
You now understand how to implement secure, scalable custom alias support. Next, we'll explore scaling reads—how to handle billions of daily redirects at consistent sub-50ms latency.