Last modified by Robert Schaub on 2026/02/08 08:30

Show last authors
1 = Zero-Cost Hosting Implementation Guide =
2
3 **Document Version:** 1.0\\
4 **Date:** 2026-01-02\\
5 **Status:** Approved - Ready for Implementation\\
6 **Target:** Pre-release beta with logged-in users, $0-5/month budget
7
8 ----
9
10 == Executive Summary ==
11
12 This guide provides a complete implementation plan for hosting FactHarbor's pre-release beta version with **near-zero infrastructure costs** ($0-5/month) while supporting 10-50 active beta users.
13
14 **Key Strategy:**\\
15 * Leverage generous free tiers from modern cloud providers
16 * Implement aggressive cost controls (rate limiting, caching, tiered LLM models)
17 * Use separated architecture to reduce AI costs by 70%
18 * Scale infrastructure costs only when revenue/funding is secured
19
20 ----
21
22 == Recommended Architecture: Fly.io Stack ==
23
24 === Why Fly.io? ===
25
26 * **True $0/month possible** with generous free tier
27 * **Works with any tech stack** (Docker-based)
28 * **Global deployment** in seconds
29 * **Includes PostgreSQL + Redis** in free tier
30 * **Auto-suspend when idle** (saves compute)
31 * **Easy migration path** to paid tier when ready
32
33 ----
34
35 == Complete Stack Overview ==
36
37 {{code}}
38 ┌──────────────────────────────────────────────────────────┐
39 │ USER BROWSER │
40 └────────────────────┬─────────────────────────────────────┘
41
42 │ HTTPS
43
44 ┌──────────────────────────────────────────────────────────┐
45 │ Cloudflare Pages (Frontend) │
46 │ • React/Vue/Svelte SPA │
47 │ • FREE: Unlimited bandwidth │
48 │ • Global CDN │
49 └────────────────────┬─────────────────────────────────────┘
50
51 │ REST API
52
53 ┌──────────────────────────────────────────────────────────┐
54 │ Fly.io App (Backend API) │
55 │ • Node.js/Python/Go/.NET │
56 │ • FREE: 3 shared-cpu VMs (256MB each) │
57 │ • Auto-suspend when idle │
58 └──────┬──────────┬──────────┬────────────────────────────┘
59 │ │ │
60 ▼ ▼ ▼
61 ┌─────────┐ ┌──────────┐ ┌─────────────────┐
62 │ Fly.io │ │ Upstash │ │ Anthropic │
63 │Postgres │ │ Redis │ │ Claude API │
64 │ │ │ │ │ │
65 │ FREE: │ │ FREE: │ │ PAY-PER-USE: │
66 │ 3GB │ │ 10k │ │ ~$2-5/mo with │
67 │ storage │ │ cmds/day │ │ optimizations │
68 └─────────┘ └──────────┘ └─────────────────┘
69 {{/code}}
70
71 **Total Monthly Cost: $0-5**
72
73 ----
74
75 == Implementation Steps ==
76
77 === Step 1: Set Up Fly.io Account and Infrastructure ===
78
79 ==== 1.1 Create Fly.io Account ====
80
81 {{code language="bash"}}
82 # Install flyctl CLI
83 # Windows (PowerShell)
84 iwr https://fly.io/install.ps1 -useb | iex
85
86 # macOS/Linux
87 curl -L https://fly.io/install.sh | sh
88
89 # Sign up (credit card required but NOT charged for free tier)
90 fly auth signup
91
92 # Or log in if you have an account
93 fly auth login
94 {{/code}}
95
96 ==== 1.2 Create PostgreSQL Database ====
97
98 {{code language="bash"}}
99 # Create a new Postgres cluster (uses free tier)
100 fly postgres create \
101 --name factharbor-db \
102 --region ord \
103 --vm-size shared-cpu-1x \
104 --volume-size 3 \
105 --initial-cluster-size 1
106
107 # Save the connection string displayed (you'll need it)
108 # Format: postgres://user:password@factharbor-db.internal:5432/dbname
109 {{/code}}
110
111 ==== 1.3 Create Redis Cache (Upstash) ====
112
113 {{code language="bash"}}
114 # Sign up at https://upstash.com (separate service, better free tier)
115 # Free tier: 10,000 commands/day, 256MB storage
116
117 # Create database via Upstash console
118 # Select: Global, REST API enabled
119 # Save connection details:
120 # - UPSTASH_REDIS_REST_URL
121 # - UPSTASH_REDIS_REST_TOKEN
122 {{/code}}
123
124 ----
125
126 === Step 2: Containerize Your Application ===
127
128 ==== 2.1 Create Dockerfile (Node.js Example) ====
129
130 {{code language="dockerfile"}}
131 # Dockerfile
132 FROM node:20-alpine AS builder
133
134 WORKDIR /app
135
136 # Copy package files
137 COPY package*.json ./
138
139 # Install dependencies
140 RUN npm ci --only=production
141
142 # Copy source code
143 COPY . .
144
145 # Build if needed (for TypeScript, etc.)
146 RUN npm run build
147
148 # Production image
149 FROM node:20-alpine
150
151 WORKDIR /app
152
153 # Copy built app
154 COPY --from=builder /app/dist ./dist
155 COPY --from=builder /app/node_modules ./node_modules
156 COPY --from=builder /app/package*.json ./
157
158 # Expose port
159 EXPOSE 8080
160
161 # Start app
162 CMD ["node", "dist/server.js"]
163 {{/code}}
164
165 ==== 2.2 Create Dockerfile (.NET Example) ====
166
167 {{code language="dockerfile"}}
168 # Dockerfile
169 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
170 WORKDIR /src
171
172 # Copy csproj and restore dependencies
173 COPY ["FactHarbor.API/FactHarbor.API.csproj", "FactHarbor.API/"]
174 RUN dotnet restore "FactHarbor.API/FactHarbor.API.csproj"
175
176 # Copy everything else and build
177 COPY . .
178 WORKDIR "/src/FactHarbor.API"
179 RUN dotnet build "FactHarbor.API.csproj" -c Release -o /app/build
180
181 # Publish
182 FROM build AS publish
183 RUN dotnet publish "FactHarbor.API.csproj" -c Release -o /app/publish
184
185 # Runtime image
186 FROM mcr.microsoft.com/dotnet/aspnet:8.0
187 WORKDIR /app
188 COPY --from=publish /app/publish .
189
190 EXPOSE 8080
191 ENTRYPOINT ["dotnet", "FactHarbor.API.dll"]
192 {{/code}}
193
194 ==== 2.3 Test Locally ====
195
196 {{code language="bash"}}
197 # Build image
198 docker build -t factharbor-api .
199
200 # Run locally
201 docker run -p 8080:8080 \
202 -e DATABASE_URL="your-connection-string" \
203 -e REDIS_URL="your-redis-url" \
204 -e ANTHROPIC_API_KEY="your-api-key" \
205 factharbor-api
206
207 # Test
208 curl http://localhost:8080/health
209 {{/code}}
210
211 ----
212
213 === Step 3: Deploy to Fly.io ===
214
215 ==== 3.1 Initialize Fly App ====
216
217 {{code language="bash"}}
218 # Create fly.toml config
219 fly launch \
220 --name factharbor-api \
221 --region ord \
222 --no-deploy
223
224 # This creates fly.toml - edit it:
225 {{/code}}
226
227 ==== 3.2 Configure fly.toml ====
228
229 {{code language="toml"}}
230 # fly.toml
231 app = "factharbor-api"
232 primary_region = "ord"
233
234 [build]
235
236 [env]
237 PORT = "8080"
238 NODE_ENV = "production"
239
240 [http_service]
241 internal_port = 8080
242 force_https = true
243 auto_stop_machines = true # Auto-suspend when idle (saves $)
244 auto_start_machines = true
245 min_machines_running = 0 # Can scale to 0 when idle
246
247 [[http_service.checks]]
248 interval = "10s"
249 timeout = "2s"
250 grace_period = "5s"
251 method = "GET"
252 path = "/health"
253
254 [[vm]]
255 memory = '256mb'
256 cpu_kind = 'shared'
257 cpus = 1
258
259 [[statics]]
260 guest_path = "/app/public"
261 url_prefix = "/static"
262 {{/code}}
263
264 ==== 3.3 Set Secrets ====
265
266 {{code language="bash"}}
267 # Set environment variables (encrypted)
268 fly secrets set \
269 DATABASE_URL="postgres://user:pass@factharbor-db.internal:5432/db" \
270 REDIS_URL="your-upstash-redis-url" \
271 REDIS_TOKEN="your-upstash-token" \
272 ANTHROPIC_API_KEY="your-claude-api-key" \
273 JWT_SECRET="$(openssl rand -base64 32)"
274 {{/code}}
275
276 ==== 3.4 Deploy ====
277
278 {{code language="bash"}}
279 # Deploy to Fly.io
280 fly deploy
281
282 # Check status
283 fly status
284
285 # View logs
286 fly logs
287
288 # Open in browser
289 fly open
290 {{/code}}
291
292 ----
293
294 === Step 4: Set Up Frontend on Cloudflare Pages ===
295
296 ==== 4.1 Build Frontend ====
297
298 {{code language="bash"}}
299 # Example with React
300 cd frontend
301
302 # Build for production
303 npm run build
304 # Output: dist/ or build/ folder
305 {{/code}}
306
307 ==== 4.2 Deploy to Cloudflare Pages ====
308
309 {{code language="bash"}}
310 # Install Wrangler CLI
311 npm install -g wrangler
312
313 # Login to Cloudflare
314 wrangler login
315
316 # Deploy
317 wrangler pages deploy dist \
318 --project-name factharbor \
319 --branch main
320
321 # Configure environment variables in Cloudflare dashboard:
322 # - VITE_API_URL=https://factharbor-api.fly.dev
323 {{/code}}
324
325 **Alternative: Use Cloudflare Pages Git Integration**
326
327 # Push frontend to GitHub
328 # Go to Cloudflare Dashboard → Pages → Create Project
329 # Connect GitHub repo
330 # Configure build:
331 #* Framework preset: React/Vue/Svelte
332 #* Build command: {{code}}npm run build{{/code}}
333 #* Build output: {{code}}dist{{/code}}
334 # Add environment variable: {{code}}VITE_API_URL{{/code}}
335 # Deploy automatically on every git push
336
337 ----
338
339 === Step 5: Implement Cost Control Measures ===
340
341 ==== 5.1 Rate Limiting (Critical!) ====
342
343 {{code language="typescript"}}
344 // rate-limiter.ts
345 import { RateLimiterRedis } from 'rate-limiter-flexible';
346 import Redis from 'ioredis';
347
348 const redis = new Redis(process.env.REDIS_URL);
349
350 // Per-user limits
351 export const userRateLimiter = new RateLimiterRedis({
352 storeClient: redis,
353 keyPrefix: 'rl:user',
354 points: 10, // 10 analyses
355 duration: 86400, // per day
356 blockDuration: 86400 // block for 1 day if exceeded
357 });
358
359 // Global limits (safety net)
360 export const globalRateLimiter = new RateLimiterRedis({
361 storeClient: redis,
362 keyPrefix: 'rl:global',
363 points: 100, // 100 total analyses
364 duration: 86400 // per day
365 });
366
367 // Middleware
368 export async function rateLimitMiddleware(req, res, next) {
369 try {
370 const userId = req.user?.id || req.ip;
371
372 // Check user limit
373 await userRateLimiter.consume(userId);
374
375 // Check global limit
376 await globalRateLimiter.consume('global');
377
378 next();
379 } catch (err) {
380 res.status(429).json({
381 error: 'Rate limit exceeded',
382 message: 'You have reached your daily analysis limit. Please try again tomorrow.'
383 });
384 }
385 }
386 {{/code}}
387
388 ==== 5.2 Budget Alerts (Anthropic API) ====
389
390 {{code language="typescript"}}
391 // budget-monitor.ts
392 const DAILY_BUDGET = 0.50; // $0.50/day = ~$15/month
393 const MONTHLY_BUDGET = 10.00;
394
395 let dailySpend = 0;
396 let monthlySpend = 0;
397
398 export function trackAIUsage(tokensUsed: number, model: string) {
399 const cost = calculateCost(tokensUsed, model);
400
401 dailySpend += cost;
402 monthlySpend += cost;
403
404 if (dailySpend > DAILY_BUDGET) {
405 console.error(`⚠️ DAILY BUDGET EXCEEDED: $${dailySpend.toFixed(2)}`);
406 // Disable AI processing until tomorrow
407 throw new Error('Daily budget exceeded');
408 }
409
410 if (monthlySpend > MONTHLY_BUDGET) {
411 console.error(`🚨 MONTHLY BUDGET EXCEEDED: $${monthlySpend.toFixed(2)}`);
412 // Send alert email
413 sendAlertEmail('Budget exceeded!');
414 }
415 }
416
417 function calculateCost(tokens: number, model: string): number {
418 const pricing = {
419 'claude-sonnet-4.5': { input: 0.003, output: 0.015 },
420 'claude-haiku-4': { input: 0.0008, output: 0.004 }
421 };
422
423 // Simplified: average of input/output
424 const avgPrice = (pricing[model].input + pricing[model].output) / 2;
425 return (tokens / 1000000) * avgPrice;
426 }
427 {{/code}}
428
429 ==== 5.3 Tiered Model Routing (40% Cost Savings) ====
430
431 {{code language="typescript"}}
432 // llm-router.ts
433 export class LLMRouter {
434 async routeRequest(task: AITask): Promise<string> {
435 switch (task.type) {
436 case 'EXTRACT_CLAIMS':
437 // Simple extraction - use Haiku (cheap)
438 return 'claude-haiku-4';
439
440 case 'EXTRACT_FACTS':
441 // Simple extraction - use Haiku (cheap)
442 return 'claude-haiku-4';
443
444 case 'UNDERSTAND_ARTICLE':
445 // Complex reasoning - use Sonnet
446 return 'claude-sonnet-4.5';
447
448 case 'GENERATE_VERDICT':
449 // Complex synthesis - use Sonnet
450 return 'claude-sonnet-4.5';
451
452 default:
453 return 'claude-sonnet-4.5';
454 }
455 }
456 }
457
458 // Usage:
459 const model = await llmRouter.routeRequest({ type: 'EXTRACT_CLAIMS' });
460 const result = await anthropic.messages.create({
461 model: model,
462 max_tokens: 1024,
463 messages: [...]
464 });
465 {{/code}}
466
467 ==== 5.4 Claim Caching (70% Cost Savings) ====
468
469 See "Separated Architecture Implementation Guide" for full details.
470
471 {{code language="typescript"}}
472 // Quick example:
473 async function analyzeClaimWithCache(claim: string): Promise<Verdict> {
474 const cached = await cache.get(claim);
475 if (cached) {
476 return cached; // Save 100% of cost for this claim
477 }
478
479 const verdict = await analyzeClaimFull(claim);
480 await cache.set(claim, verdict, 7); // 7-day TTL
481 return verdict;
482 }
483 {{/code}}
484
485 ----
486
487 === Step 6: Authentication for Beta Users ===
488
489 ==== 6.1 Simple JWT-based Auth ====
490
491 {{code language="typescript"}}
492 // auth.ts
493 import jwt from 'jsonwebtoken';
494 import bcrypt from 'bcrypt';
495
496 const JWT_SECRET = process.env.JWT_SECRET;
497
498 // Beta user allowlist (stored in DB)
499 const BETA_USERS = [
500 { email: 'user1@example.com', password: '$2b$10$...' },
501 { email: 'user2@example.com', password: '$2b$10$...' }
502 ];
503
504 export async function login(email: string, password: string) {
505 const user = await db.query(
506 'SELECT * FROM users WHERE email = $1 AND is_beta_user = true',
507 [email]
508 );
509
510 if (!user.rows[0]) {
511 throw new Error('Not authorized for beta');
512 }
513
514 const valid = await bcrypt.compare(password, user.rows[0].password_hash);
515 if (!valid) {
516 throw new Error('Invalid credentials');
517 }
518
519 const token = jwt.sign(
520 { userId: user.rows[0].id, email: user.rows[0].email },
521 JWT_SECRET,
522 { expiresIn: '7d' }
523 );
524
525 return { token, user: user.rows[0] };
526 }
527
528 export function authenticateToken(req, res, next) {
529 const token = req.headers.authorization?.split(' ')[1];
530
531 if (!token) {
532 return res.status(401).json({ error: 'No token provided' });
533 }
534
535 try {
536 const decoded = jwt.verify(token, JWT_SECRET);
537 req.user = decoded;
538 next();
539 } catch (err) {
540 return res.status(403).json({ error: 'Invalid token' });
541 }
542 }
543 {{/code}}
544
545 ==== 6.2 Beta User Signup (Manual Approval) ====
546
547 {{code language="typescript"}}
548 // POST /api/beta-signup
549 export async function betaSignup(req, res) {
550 const { email, name, reason } = req.body;
551
552 // Store request for manual review
553 await db.query(`
554 INSERT INTO beta_signup_requests (email, name, reason, status)
555 VALUES ($1, $2, $3, 'pending')
556 `, [email, name, reason]);
557
558 res.json({
559 message: 'Thank you! Your request has been submitted for review.'
560 });
561
562 // Notify admin
563 await sendEmail({
564 to: 'admin@factharbor.org',
565 subject: 'New beta signup request',
566 body: `${name} (${email}) requested beta access: ${reason}`
567 });
568 }
569
570 // Admin approves via /admin/beta-requests
571 export async function approveBetaUser(req, res) {
572 const { requestId } = req.params;
573 const { approved } = req.body;
574
575 if (approved) {
576 // Create user account
577 const tempPassword = generateRandomPassword();
578 const passwordHash = await bcrypt.hash(tempPassword, 10);
579
580 await db.query(`
581 INSERT INTO users (email, password_hash, is_beta_user)
582 SELECT email, $1, true
583 FROM beta_signup_requests
584 WHERE id = $2
585 `, [passwordHash, requestId]);
586
587 // Send welcome email with temp password
588 const request = await db.query(
589 'SELECT email FROM beta_signup_requests WHERE id = $1',
590 [requestId]
591 );
592
593 await sendEmail({
594 to: request.rows[0].email,
595 subject: 'Welcome to FactHarbor Beta!',
596 body: `Your temporary password: ${tempPassword}\n\nPlease log in and change it.`
597 });
598 }
599
600 // Update request status
601 await db.query(
602 'UPDATE beta_signup_requests SET status = $1 WHERE id = $2',
603 [approved ? 'approved' : 'rejected', requestId]
604 );
605
606 res.json({ success: true });
607 }
608 {{/code}}
609
610 ----
611
612 === Step 7: Monitoring and Alerts ===
613
614 ==== 7.1 Health Check Endpoint ====
615
616 {{code language="typescript"}}
617 // GET /health
618 export async function healthCheck(req, res) {
619 const checks = {
620 api: 'ok',
621 database: await checkDatabase(),
622 redis: await checkRedis(),
623 budgetStatus: await checkBudget()
624 };
625
626 const allHealthy = Object.values(checks).every(v => v === 'ok');
627
628 res.status(allHealthy ? 200 : 503).json({
629 status: allHealthy ? 'healthy' : 'degraded',
630 checks,
631 timestamp: new Date().toISOString()
632 });
633 }
634
635 async function checkDatabase(): Promise<string> {
636 try {
637 await db.query('SELECT 1');
638 return 'ok';
639 } catch (err) {
640 return 'error';
641 }
642 }
643
644 async function checkBudget(): Promise<string> {
645 const today = await db.query(`
646 SELECT SUM(cost) as total
647 FROM ai_usage_log
648 WHERE date = CURRENT_DATE
649 `);
650
651 const spent = today.rows[0]?.total || 0;
652 if (spent > DAILY_BUDGET) return 'exceeded';
653 if (spent > DAILY_BUDGET * 0.8) return 'warning';
654 return 'ok';
655 }
656 {{/code}}
657
658 ==== 7.2 Daily Budget Report ====
659
660 {{code language="typescript"}}
661 // Run daily via cron job or scheduled task
662 export async function sendDailyReport() {
663 const stats = await db.query(`
664 SELECT
665 COUNT(*) as total_analyses,
666 COUNT(DISTINCT user_id) as active_users,
667 SUM(cost) as total_cost,
668 AVG(processing_time_ms) as avg_processing_time
669 FROM analysis_log
670 WHERE date = CURRENT_DATE - INTERVAL '1 day'
671 `);
672
673 const cacheStats = await db.query(`
674 SELECT
675 COUNT(*) as total_claims,
676 SUM(access_count - 1) as cache_hits
677 FROM ClaimVerdict
678 WHERE created_at >= CURRENT_DATE - INTERVAL '1 day'
679 `);
680
681 await sendEmail({
682 to: 'admin@factharbor.org',
683 subject: `FactHarbor Daily Report - ${new Date().toLocaleDateString()}`,
684 body: `
685 Total Analyses: ${stats.rows[0].total_analyses}
686 Active Users: ${stats.rows[0].active_users}
687 AI Cost: $${stats.rows[0].total_cost.toFixed(2)}
688 Cache Hits: ${cacheStats.rows[0].cache_hits}
689 Avg Processing Time: ${stats.rows[0].avg_processing_time}ms
690 `
691 });
692 }
693 {{/code}}
694
695 ==== 7.3 Set Up Fly.io Monitoring ====
696
697 {{code language="bash"}}
698 # View metrics
699 fly dashboard
700
701 # Set up alerts (in Fly.io dashboard)
702 # Alert if:
703 # - Response time > 2s
704 # - Error rate > 5%
705 # - Memory usage > 200MB
706 {{/code}}
707
708 ----
709
710 == Cost Breakdown Analysis ==
711
712 === Infrastructure Costs ===
713
714 |= Service |= Free Tier |= Usage Estimate |= Monthly Cost
715 | **Fly.io App** | 3 VMs (256MB each) | 1 VM used | **$0**
716 | **Fly.io Postgres** | 3GB storage | 1GB used | **$0**
717 | **Upstash Redis** | 10k cmds/day | ~5k/day | **$0**
718 | **Cloudflare Pages** | Unlimited | Frontend hosting | **$0**
719 | **Domain (optional)** | N/A | factharbor.org | ~$12/year
720
721 **Total Infrastructure: $0/month** (or $1/month if you count domain)
722
723 ----
724
725 === AI Costs (Claude API) ===
726
727 **Scenario:** 50 beta users, 10 analyses/user/month = 500 total analyses
728
729 **With All Optimizations:**\\
730 * Claim caching (70% hit rate after 1 week)
731 * Tiered models (Haiku for extraction, Sonnet for reasoning)
732
733 |= Stage |= Model |= Tokens/Analysis |= Cost/1M tokens |= Cost/Analysis |= Total (500)
734 | Extract Claims | Haiku | 2,000 | $0.80 | $0.0016 | $0.80
735 | Extract Facts | Haiku | 5,000 | $0.80 | $0.0040 | $2.00
736 | Generate Verdict | Sonnet | 3,000 | $3.00 | $0.0090 | $4.50
737
738 **Before caching:** $7.30/month\\
739 **After 70% cache hit rate:** $7.30 × 0.30 = **$2.19/month**
740
741 ----
742
743 === Total Monthly Cost: $2-3 ===
744
745 **Best case:** $2.19 (with high cache hit rate)\\
746 **Worst case:** $7.30 (no caching, month 1)\\
747 **Realistic:** $3-5 (moderate caching)
748
749 ----
750
751 == Scaling Plan ==
752
753 === When to Upgrade? ===
754
755 |= Metric |= Free Tier Limit |= Action When Reached
756 | **Users** | 50-100 | Stay on free tier, add waitlist
757 | **Analyses/day** | 100 | Increase rate limits slowly
758 | **Database size** | 3GB | Archive old data, or upgrade to $7/mo tier
759 | **Memory usage** | 256MB | Optimize code, or add 1 more free VM
760 | **Monthly AI cost** | $10 | Seek funding/donations before scaling
761
762 === Upgrade Path ===
763
764 **Phase 1: Free Tier (Current)**\\
765 * 0-50 users
766 * $0-5/month
767 * Manual beta approvals
768
769 **Phase 2: Hobby Tier ($10-20/month)**\\
770 * 50-200 users
771 * Upgrade Fly.io to 512MB VMs ($5/mo)
772 * Upgrade Upstash to paid tier ($10/mo)
773 * AI costs: $5-10/month
774
775 **Phase 3: Growth Tier ($50-100/month)**\\
776 * 200-1000 users
777 * Add CDN, monitoring, backups
778 * Consider sponsorships/donations
779
780 ----
781
782 == Deployment Checklist ==
783
784 === Pre-Deployment ===
785
786 * [ ] Backend API containerized and tested locally
787 * [ ] Frontend built and tested
788 * [ ] Database schema created
789 * [ ] Environment variables documented
790 * [ ] Rate limiting implemented
791 * [ ] Budget monitoring implemented
792 * [ ] Authentication system tested
793
794 === Fly.io Deployment ===
795
796 * [ ] Fly.io account created
797 * [ ] PostgreSQL database created
798 * [ ] Upstash Redis created
799 * [ ] Secrets configured ({{code}}fly secrets set{{/code}})
800 * [ ] {{code}}fly.toml{{/code}} configured
801 * [ ] Health check endpoint working
802 * [ ] Deployed ({{code}}fly deploy{{/code}})
803 * [ ] Logs reviewed ({{code}}fly logs{{/code}})
804
805 === Cloudflare Pages Deployment ===
806
807 * [ ] Frontend repo pushed to GitHub
808 * [ ] Cloudflare Pages connected to repo
809 * [ ] Build settings configured
810 * [ ] Environment variables set
811 * [ ] Custom domain configured (optional)
812 * [ ] HTTPS enabled
813
814 === Post-Deployment ===
815
816 * [ ] Health check returns 200
817 * [ ] Frontend loads correctly
818 * [ ] API requests work
819 * [ ] Authentication works
820 * [ ] Rate limiting works
821 * [ ] Budget alerts configured
822 * [ ] Daily reports configured
823 * [ ] Backup strategy defined
824
825 ----
826
827 == Troubleshooting ==
828
829 === Issue: Fly.io App Not Starting ===
830
831 {{code language="bash"}}
832 # Check logs
833 fly logs
834
835 # Common issues:
836 # - Wrong PORT (must be 8080)
837 # - Missing environment variables
838 # - Database connection failed
839
840 # Debug locally:
841 fly ssh console
842 {{/code}}
843
844 === Issue: Database Connection Failed ===
845
846 {{code language="bash"}}
847 # Verify database is running
848 fly postgres list
849
850 # Check connection string
851 fly postgres connect -a factharbor-db
852
853 # Test from app
854 fly ssh console -a factharbor-api
855 # Inside container:
856 psql $DATABASE_URL
857 {{/code}}
858
859 === Issue: Rate Limits Not Working ===
860
861 {{code language="typescript"}}
862 // Verify Redis connection
863 import Redis from 'ioredis';
864 const redis = new Redis(process.env.REDIS_URL);
865
866 redis.ping().then(() => {
867 console.log('Redis connected!');
868 }).catch(err => {
869 console.error('Redis error:', err);
870 });
871 {{/code}}
872
873 === Issue: High AI Costs ===
874
875 # Check cache hit rate:
876 {{code language="sql"}}
877 SELECT
878 COUNT(*) as total_claims,
879 AVG(access_count) as avg_reuses
880 FROM ClaimVerdict;
881 {{/code}}
882
883 # Verify tiered model routing:
884 {{code language="typescript"}}
885 // Log every LLM call
886 console.log(`AI Request: ${task.type} → ${model} → ${tokens} tokens → $${cost}`);
887 {{/code}}
888
889 # Implement hard budget limit:
890 {{code language="typescript"}}
891 if (dailySpend > DAILY_BUDGET) {
892 throw new Error('Budget exceeded - AI disabled for today');
893 }
894 {{/code}}
895
896 ----
897
898 == Alternative: Even Cheaper Option (Vercel + Supabase) ==
899
900 If Fly.io seems too complex:
901
902 {{code}}
903 Frontend: Vercel (free, better DX than Cloudflare)
904 Backend: Vercel Serverless Functions (free tier: 100GB-hrs)
905 Database: Supabase (free tier: 500MB, 2 projects)
906 Redis: Upstash (same as above)
907
908 Pros: Even simpler deployment (git push)
909 Cons: Less control, harder to migrate later
910 {{/code}}
911
912 ----
913
914 == Security Considerations ==
915
916 === 1. Protect API Keys ===
917
918 {{code language="bash"}}
919 # NEVER commit secrets to git
920 echo ".env" >> .gitignore
921 echo "fly.toml" >> .gitignore # Contains secrets
922
923 # Use fly secrets instead
924 fly secrets set ANTHROPIC_API_KEY="sk-ant-..."
925 {{/code}}
926
927 === 2. Enable CORS Properly ===
928
929 {{code language="typescript"}}
930 import cors from 'cors';
931
932 app.use(cors({
933 origin: [
934 'https://factharbor.pages.dev',
935 'https://factharbor.org'
936 ],
937 credentials: true
938 }));
939 {{/code}}
940
941 === 3. Rate Limit All Endpoints ===
942
943 {{code language="typescript"}}
944 // Not just /analyze, but also /login, /signup
945 app.use('/api/login', rateLimitMiddleware);
946 app.use('/api/analyze', rateLimitMiddleware);
947 app.use('/api/beta-signup', rateLimitMiddleware);
948 {{/code}}
949
950 === 4. Validate All Inputs ===
951
952 {{code language="typescript"}}
953 import { z } from 'zod';
954
955 const AnalyzeRequestSchema = z.object({
956 url: z.string().url(),
957 userId: z.string().uuid().optional()
958 });
959
960 app.post('/api/analyze', async (req, res) => {
961 const result = AnalyzeRequestSchema.safeParse(req.body);
962 if (!result.success) {
963 return res.status(400).json({ error: result.error });
964 }
965
966 // Process validated data
967 const { url } = result.data;
968 // ...
969 });
970 {{/code}}
971
972 ----
973
974 == Success Metrics ==
975
976 Track these weekly:
977
978 * [ ] **Uptime:** &gt;99% (check Fly.io status)
979 * [ ] **Response time:** &lt;2s average (check logs)
980 * [ ] **Daily cost:** &lt;$0.50 (check budget monitor)
981 * [ ] **Cache hit rate:** &gt;40% after week 2
982 * [ ] **Active users:** Growing steadily
983 * [ ] **Error rate:** &lt;1%
984
985 ----
986
987 == Next Steps After Beta ==
988
989 When ready to scale:
990
991 # **Seek funding/donations** before increasing usage limits
992 # **Add payment system** (Stripe) if going subscription model
993 # **Upgrade infrastructure** gradually based on metrics
994 # **Implement CDN** for faster global access
995 # **Add monitoring** (Sentry, DataDog, etc.)
996 # **Hire DevOps** if growing beyond 1000 users
997
998 ----
999
1000 == Resources ==
1001
1002 * **Fly.io Docs:** https://fly.io/docs
1003 * **Upstash Docs:** https://docs.upstash.com
1004 * **Cloudflare Pages:** https://pages.cloudflare.com
1005 * **Anthropic Pricing:** https://www.anthropic.com/pricing
1006 * **Rate Limiter Library:** https://github.com/animir/node-rate-limiter-flexible
1007
1008 ----
1009
1010 == Support Contacts ==
1011
1012 * **Fly.io Community:** https://community.fly.io
1013 * **Fly.io Support:** support@fly.io (for paying customers)
1014 * **This Guide Author:** Claude Code (2026-01-02)
1015
1016 ----
1017
1018 == Change Log ==
1019
1020 |= Version |= Date |= Author |= Changes
1021 | 1.0 | 2026-01-02 | Claude Code | Initial hosting guide for zero-cost beta