Backend API for LinkTome - a modern Linktree alternative built on Azure.
Frontend Repository: Zacgoose/linktome
🚀 New Feature Planning: Agency/Multi-Account Profiles - See Planning Documentation
LinkTome API is an Azure Function App built with PowerShell 7.4 that provides:
- User authentication with JWT tokens
- Profile management
- Link management for personal link-in-bio pages
- Secure Azure Table Storage integration
- Runtime: PowerShell 7.4
- Platform: Azure Functions v4
- Storage: Azure Table Storage
Users- User accounts and profiles (includes 2FA settings)Links- User link collectionsShortLinks- URL shortener service (slug-based short links)TwoFactorSessions- Temporary 2FA verification sessionsRateLimits- IP-based rate limiting trackingSecurityEvents- Security event audit logAnalytics- Page views, link clicks, and short link redirect trackingFeatureUsage- Feature access tracking for tier validation
- Authentication: JWT with PBKDF2-SHA256 password hashing
- Frontend Integration: Azure Static Web Apps (handles CORS and security headers)
- Rate Limiting: IP-based using Azure Table Storage (5 login/min, 3 signup/hour)
- Logging: Azure Table Storage for security events and analytics
- Subscription Tiers: Free, Premium, and Enterprise tiers with feature-based access control
- ✅ JWT-based authentication with Bearer tokens
- ✅ Strong password hashing (PBKDF2-SHA256, 100K iterations)
- ✅ Two-Factor Authentication (2FA) support
- ✅ Email-based 2FA with 6-digit codes (SHA-256 hashed)
- ✅ TOTP-based 2FA (compatible with Google Authenticator, Authy, etc.)
- ✅ TOTP secrets encrypted at rest (AES-256)
- ✅ Support for dual 2FA (both email and TOTP enabled)
- ✅ Backup codes for account recovery (SHA-256 hashed, single-use)
- ✅ Secure session management with expiration
- ✅ 2FA is optional (user opt-in)
- ✅ Input validation and sanitization
- ✅ Protection against NoSQL injection
- ✅ Rate limiting (5 login attempts/min, 3 signups/hour per IP)
- ✅ Security event logging to Azure Table Storage
- ✅ Safe error handling (no information disclosure)
- ✅ Minimum password requirements
- ✅ Automatic page view tracking on profile loads
- ✅ Link click tracking via public endpoint
- ✅ Short link redirect tracking with detailed analytics
- ✅ Server-side analytics storage
- ✅ Tracks IP address, user agent, and referrer
- ✅ Analytics dashboard data via admin endpoint (views, clicks, popular links)
- ✅ Dashboard statistics (total links, views, clicks, unique visitors)
- ✅ Time-series data (views and clicks by day)
- ✅ Advanced analytics restricted to Pro/Premium/Enterprise tiers
- ✅ Create short links with auto-generated slugs
- ✅ Tier-based limits (Free: not available, Pro: 5, Premium: 20, Enterprise: unlimited)
- ✅ Click tracking and analytics
- ✅ Active/inactive toggle for links
- ✅ Public redirect endpoint
- ✅ Detailed analytics (Pro+ tiers)
- ✅ Four-tier system: Free, Pro, Premium, Enterprise
- ✅ Tier-based feature access control
- ✅ Link limits by tier (Free: 10, Pro: 50, Premium: 100, Enterprise: unlimited)
- ✅ Short link limits by tier (Free: not available, Pro: 5, Premium: 20, Enterprise: unlimited)
- ✅ Advanced analytics for Pro/Premium/Enterprise users only
- ✅ Feature usage tracking and analytics
- ✅ Automatic subscription expiration handling
- ✅ Graceful degradation for expired subscriptions
- ✅ Automatic feature flagging on subscription downgrade (data preserved)
- 📄 See TIER_SYSTEM.md for complete documentation
- 📄 See SUBSCRIPTION_CLEANUP.md for feature handling documentation
- ✅ Stripe Checkout for subscription payments
- ✅ Stripe Customer Portal for subscription management
- ✅ Webhook handling for automatic subscription updates
- ✅ Support for monthly and annual billing cycles
- ✅ Automatic subscription sync and renewal monitoring
- ✅ Cancel at period end (users keep access until billing period ends)
- ✅ Payment failure handling with automatic feature flagging
- ✅ Data preservation on downgrade for easy restoration
- ✅ Secure webhook signature verification
- 📄 See STRIPE_SETUP.md for configuration guide
- ✅ Appearance customization (theme: light/dark)
- ✅ Button style options (rounded, square, pill)
- ✅ Custom colors (background, text, buttons)
- ✅ Settings applied to public profile pages
POST /public/signup- Register new userPOST /public/login- Authenticate user (returns 2FA session if enabled)POST /public/2fatoken?action=verify- Verify 2FA code and complete authenticationPOST /public/2fatoken?action=resend- Resend 2FA email codeGET /public/getUserProfile?username={username}- Get public profile and links (auto-tracks page view)POST /public/trackLinkClick- Track link click analytics (requires username and linkId)GET /public/l?slug={slug}- Redirect short link to target URL (auto-tracks redirect analytics)POST /public/stripeWebhook- Handle Stripe webhook events (signature verified)
GET /admin/getProfile- Get authenticated user's profilePUT /admin/updateProfile- Update profile (displayName, bio, avatar)GET /admin/getLinks- Get user's linksPUT /admin/updateLinks- Create, update, or delete linksGET /admin/getShortLinks- Get user's short links with usage statisticsPUT /admin/updateShortLinks- Create, update, or delete short linksGET /admin/getShortLinkAnalytics?slug={slug}- Get detailed analytics for short links (Pro+ tiers)GET /admin/getAnalytics- Get analytics data (page views, link clicks, unique visitors, views/clicks by day, most popular links)GET /admin/getDashboardStats- Get dashboard statistics (total links, views, visitors)GET /admin/getAppearance- Get appearance settings (theme, colors, button style)PUT /admin/updateAppearance- Update appearance settingsPOST /admin/2fatokensetup?action=setup- Setup 2FA (generates TOTP secret, QR code, backup codes)POST /admin/2fatokensetup?action=enable- Enable 2FA after verificationPOST /admin/2fatokensetup?action=disable- Disable 2FA
GET /admin/getSubscription- Get current subscription detailsPOST /admin/createCheckoutSession- Create Stripe Checkout session for subscription upgradePOST /admin/createPortalSession- Create Stripe Customer Portal session (manage subscription, cancel, update payment method, view invoices)POST /admin/upgradeSubscription- Manually upgrade subscription (for internal/admin use)
- PowerShell 7.4+
- Azure Functions Core Tools v4
- Azure Storage Emulator or Azurite
- Visual Studio Code (recommended)
- Azure Functions Extension for VS Code (recommended)
-
Clone the repository
git clone https://github.com/Zacgoose/linktome-api.git cd linktome-api -
Configure local settings
The repository includes
local.settings.jsonwith development defaults:{ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "powershell", "FUNCTIONS_WORKER_RUNTIME_VERSION": "7.4", "AzureWebJobsStorage": "UseDevelopmentStorage=true", "JWT_SECRET": "dev-secret-change-in-production-please-make-this-very-long-and-random-at-least-64-characters" } } -
Start Azure Storage Emulator
# Option 1: Azurite (recommended) azurite --silent --location ./azurite --debug ./azurite/debug.log # Option 2: Azure Storage Emulator (Windows only) AzureStorageEmulator.exe start
-
Start the Function App
func start
-
Test the API
# Health check (get a public profile - will return 404 if no users exist) curl http://localhost:7071/api/public/getUserProfile?username=testuser # Create a test user curl -X POST http://localhost:7071/api/public/signup \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com","username":"testuser","password":"TestPass123"}'
All environment variables are configured in local.settings.json:
| Variable | Description | Default |
|---|---|---|
FUNCTIONS_WORKER_RUNTIME |
Runtime for Azure Functions | powershell |
FUNCTIONS_WORKER_RUNTIME_VERSION |
PowerShell version | 7.4 |
AzureWebJobsStorage |
Storage connection string | UseDevelopmentStorage=true |
JWT_SECRET |
Secret key for JWT signing | Dev secret (64+ chars) |
AZURE_FUNCTIONS_ENVIRONMENT |
Environment indicator | Development (implicit) |
ENCRYPTION_KEY |
AES-256 key for encrypting TOTP secrets | Exactly 32 characters required |
SMTP_SERVER |
SMTP server for 2FA emails | Required for email 2FA |
SMTP_PORT |
SMTP port (usually 587) | Required for email 2FA |
SMTP_USERNAME |
SMTP username | Required for email 2FA |
SMTP_PASSWORD |
SMTP password | Required for email 2FA |
SMTP_FROM |
Sender email address | Required for email 2FA |
STRIPE_API_KEY |
Stripe API key | Required for Stripe integration |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret | Required for webhook verification |
STRIPE_PRICE_ID_PRO |
Stripe price ID for Pro (monthly) | Required for Pro tier checkout |
STRIPE_PRICE_ID_PRO_ANNUAL |
Stripe price ID for Pro (annual) | Required for Pro annual checkout |
STRIPE_PRICE_ID_PREMIUM |
Stripe price ID for Premium (monthly) | Required for Premium tier checkout |
STRIPE_PRICE_ID_PREMIUM_ANNUAL |
Stripe price ID for Premium (annual) | Required for Premium annual checkout |
STRIPE_PRICE_ID_ENTERPRISE |
Stripe price ID for Enterprise (monthly) | Required for Enterprise tier checkout |
STRIPE_PRICE_ID_ENTERPRISE_ANNUAL |
Stripe price ID for Enterprise (annual) | Required for Enterprise annual checkout |
FRONTEND_URL |
Frontend URL for redirect after checkout | http://localhost:3000 |
# Sign up
curl -X POST http://localhost:7071/api/public/signup \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","username":"johndoe","password":"SecurePass123"}'
# Response includes accessToken
# {"user":{"UserId":"...","email":"...","username":"..."},"accessToken":"eyJ..."}
# Login
curl -X POST http://localhost:7071/api/public/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"SecurePass123"}'
# Get profile (authenticated - use token from signup/login)
curl http://localhost:7071/api/admin/getProfile \
-H "Authorization: Bearer eyJ..."
# Update profile
curl -X PUT http://localhost:7071/api/admin/updateProfile \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"displayName":"John Doe","bio":"My bio","avatar":"https://example.com/avatar.jpg"}'
# Get public profile
curl "http://localhost:7071/api/public/getUserProfile?username=johndoe"Import the collection (if available) or manually create requests using the endpoints above.
linktome-api/
├── HttpTrigger/ # Azure Function trigger configuration
│ └── function.json # HTTP trigger binding
├── Modules/
│ ├── LinkTomeCore/ # Core functionality
│ │ ├── Auth/ # JWT and authentication
│ │ ├── Table/ # Azure Table Storage helpers
│ │ ├── Validation/ # Input validation
│ │ ├── Security/ # Security headers and CORS
│ │ └── Error/ # Safe error handling
│ ├── LinkTomeEntrypoints/ # Request routing
│ ├── PublicApi/ # Public endpoint handlers
│ ├── PrivateApi/ # Admin endpoint handlers
│ ├── AzBobbyTables/ # Azure Table Storage wrapper
│ └── PSJsonWebToken/ # JWT library
├── Shared/ # Shared resources
├── Tools/ # Development tools
├── host.json # Function App configuration
├── local.settings.json # Local environment variables
├── profile.ps1 # Startup script
├── requirements.psd1 # Module dependencies
└── version_latest.txt # API version
Documentation:
├── SECURITY_REVIEW.md # Comprehensive security analysis
├── SECURITY_IMPLEMENTATION_ROADMAP.md # Implementation guide
├── DEPLOYMENT_SECURITY_CHECKLIST.md # Deployment checklist
└── README.md # This file
This API implements comprehensive security measures:
- Input Validation: All inputs are validated and sanitized
- Authentication: JWT tokens with 24-hour expiration
- Password Security: PBKDF2-SHA256 with 100,000 iterations
- Query Protection: Azure Table Storage queries are sanitized to prevent NoSQL injection
- Rate Limiting: IP-based limits on login and signup endpoints
- Security Event Logging: Comprehensive audit trail in Azure Table Storage
- Error Handling: Safe error messages (no information disclosure in production)
- Security Headers & CORS: Handled automatically by Azure Static Web Apps integration
For detailed security information, see:
- SECURITY_REVIEW.md - Complete security analysis
- DEPLOYMENT_SECURITY_CHECKLIST.md - Production deployment checklist
- Azure subscription
- Azure CLI installed and configured
- Azure Functions Core Tools v4
See DEPLOYMENT_SECURITY_CHECKLIST.md for complete deployment instructions.
Quick Start:
# Deploy to Azure Function App
func azure functionapp publish <function-app-name>Important: Before deploying to production:
- Generate a strong JWT_SECRET (128+ characters)
- Configure environment variables in Azure Portal
- Enable HTTPS only
- Set minimum TLS to 1.2
- Link with Azure Static Web App for automatic CORS handling
| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
✅ Yes | JWT signing secret (min 64 chars, 128+ recommended) |
AzureWebJobsStorage |
✅ Yes | Azure Storage connection string (auto-configured) |
AZURE_FUNCTIONS_ENVIRONMENT |
✅ Yes | Set to Production |
CORS_ALLOWED_ORIGINS |
Only if NOT using Azure Static Web Apps | |
ENCRYPTION_KEY |
AES-256 key for TOTP secrets (exactly 32 chars, required for TOTP 2FA) | |
SMTP_SERVER |
SMTP server for 2FA emails (required for email 2FA) | |
SMTP_PORT |
SMTP port (required for email 2FA) | |
SMTP_USERNAME |
SMTP username (required for email 2FA) | |
SMTP_PASSWORD |
SMTP password (required for email 2FA) | |
SMTP_FROM |
Sender email address (required for email 2FA) |
All requests and responses use JSON.
Admin endpoints require JWT authentication via Bearer token:
Authorization: Bearer <jwt-token>
{
"error": "Error message"
}-
Sign Up
POST /public/signup { "email": "user@example.com", "username": "johndoe", "password": "SecurePass123" }Response:
{ "user": { "UserId": "user-abc123", "email": "user@example.com", "username": "johndoe" }, "accessToken": "eyJhbGciOi..." } -
Update Profile
PUT /admin/updateProfile Authorization: Bearer eyJhbGciOi... { "displayName": "John Doe", "bio": "Software Developer | Tech Enthusiast", "avatar": "https://example.com/avatar.jpg" } -
Add Links
PUT /admin/updateLinks Authorization: Bearer eyJhbGciOi... { "links": [ {"operation": "add", "title": "GitHub", "url": "https://github.com/johndoe", "order": 1, "active": true}, {"operation": "add", "title": "Twitter", "url": "https://twitter.com/johndoe", "order": 2, "active": true} ] } -
View Public Profile
GET /public/getUserProfile?username=johndoeResponse:
{ "username": "johndoe", "displayName": "John Doe", "bio": "Software Developer | Tech Enthusiast", "avatar": "https://example.com/avatar.jpg", "links": [ {"id": "link-123", "title": "GitHub", "url": "https://github.com/johndoe", "order": 1}, {"id": "link-456", "title": "Twitter", "url": "https://twitter.com/johndoe", "order": 2} ] }
-
Create Short Link (Pro tier required)
PUT /admin/updateShortLinks Authorization: Bearer eyJhbGciOi... { "shortLinks": [ { "operation": "add", "targetUrl": "https://github.com/johndoe/awesome-project", "title": "My Awesome Project", "active": true } ] }Response (includes auto-generated slug):
{ "success": true, "created": [ { "slug": "a3x9k2", "targetUrl": "https://github.com/johndoe/awesome-project", "title": "My Awesome Project" } ] } -
List Short Links
GET /admin/getShortLinks Authorization: Bearer eyJhbGciOi...
Response:
{ "shortLinks": [ { "slug": "a3x9k2", "targetUrl": "https://github.com/johndoe/awesome-project", "title": "My Awesome Project", "active": true, "clicks": 42, "createdAt": "2024-01-15T10:30:00Z", "lastClickedAt": "2024-01-20T14:25:00Z" } ], "total": 1 } -
Public Redirect (Anyone can access)
GET /public/l?slug=a3x9k2Response: HTTP 301 Redirect to target URL
Location: https://github.com/johndoe/awesome-project -
Get Analytics (Pro+ tiers only)
GET /admin/getShortLinkAnalytics?slug=a3x9k2 Authorization: Bearer eyJhbGciOi...Response:
{ "summary": { "totalRedirects": 42, "uniqueVisitors": 28 }, "hasAdvancedAnalytics": true, "topShortLinks": [ { "slug": "a3x9k2", "targetUrl": "https://github.com/johndoe/awesome-project", "clicks": 42 } ], "redirectsByDay": [ {"date": "2024-01-15", "clicks": 5}, {"date": "2024-01-16", "clicks": 12}, {"date": "2024-01-17", "clicks": 25} ], "topReferrers": [ {"referrer": "https://twitter.com", "count": 18}, {"referrer": "https://reddit.com", "count": 10} ] }
Request (Add Operation):
{
"shortLinks": [
{
"operation": "add",
"targetUrl": "https://example.com/very/long/url",
"title": "Optional title",
"active": true
}
]
}Response (Add Operation):
{
"success": true,
"created": [
{
"slug": "a3x9k2",
"targetUrl": "https://example.com/very/long/url",
"title": "Optional title"
}
]
}Request (Update/Remove Operations):
{
"shortLinks": [
{
"operation": "update", // or "remove"
"slug": "a3x9k2", // Required: 6-char auto-generated slug
"targetUrl": "https://...", // Optional for update
"title": "Updated title", // Optional for update
"active": false // Optional for update
}
]
}Validation Rules:
slug: Auto-generated 6-character string (lowercase letters and numbers)- 2.18 billion possible combinations (36^6)
targetUrl: Valid http/https URL, max 2048 characterstitle: Optional, max 100 characters
Tier Limits:
- Free: Not available (upgrade to Pro required)
- Pro: 5 short links
- Premium: 20 short links
- Enterprise: Unlimited short links
Error Responses:
{
"error": "Short links are not available on the Free plan. Upgrade to Pro or higher to create short links.",
"upgradeRequired": true,
"currentTier": "free",
"feature": "shortLinks"
}{
"error": "Short link limit exceeded. Your Pro plan allows up to 5 short links. You currently have 5 short links.",
"currentCount": 5,
"limit": 5
}Issue: "Function not found" error
- Solution: Ensure module imports are working. Check
profile.ps1execution logs.
Issue: "JWT_SECRET must be at least 64 characters"
- Solution: Generate a strong secret:
openssl rand -base64 96
Issue: "Cannot connect to storage"
- Solution: Ensure Azure Storage Emulator or Azurite is running
Issue: 401 Unauthorized on admin endpoints
- Solution: Verify JWT token is included in Authorization header as
Bearer <token>
Local development:
# Logs are displayed in console where `func start` is runningAzure:
# View logs in Azure Portal → Function App → Monitor → Logs
# View security events in Azure Table Storage → SecurityEvents table- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
If you discover a security vulnerability:
- DO NOT open a public issue
- Email security details to [your-security-email]
- Include steps to reproduce and potential impact
[Specify your license here]
- Issues: https://github.com/Zacgoose/linktome-api/issues
- Discussions: https://github.com/Zacgoose/linktome-api/discussions
- Frontend: https://github.com/Zacgoose/linktome
- PowerShell JWT implementation: PSJsonWebToken
- Azure Table Storage wrapper: AzBobbyTables (bundled)
- Inspired by Linktree and similar link-in-bio services
Version: 1.0.0
Last Updated: December 21, 2025