Replacing Firebase with a Production-Grade AI Call Pipeline in 12 Weeks
An AI voice platform for home service businesses had a working prototype and paying customers. The infrastructure could not survive them. This is how we rebuilt it — and what that unlocked.
Client details anonymised at their request. Metrics, timeline, and technical architecture are accurate.
What the Client Had Built
The client is an early-stage B2B SaaS company building AI agents that handle inbound calls for home service businesses — HVAC, plumbing, electrical, pest control. The agents book jobs, handle after-hours calls, detect emergencies, and coach human CSRs in real time.
By early 2024, they had raised pre-seed funding, signed three beta customers, and built a working prototype. The prototype ran on Firebase Realtime Database and Google Cloud Functions — reasonable choices for a concept validation, but structurally unable to carry production load or support the integrations enterprise customers required.
The team was a CEO and two engineers, both frontend-focused. A Series A close was 4 months out. The gap between what they had and what they needed to demonstrate was too large to close by hiring — senior backend and ML engineers take 4–9 months to recruit. They needed a different path.
What Was Working
- Pre-seed raised — runway secured for the sprint
- Concept validated: 3 paying beta customers
- AI prototype handling basic call flows
- Domain expertise: deep understanding of home services ops
What Was Missing
- No production-grade backend — Firebase crashing at scale
- No real-time processing — 5-minute batch delay on all calls
- No reliable CRM sync — 30% of bookings failing
- No analytics — could not prove ROI to customers or investors
Four Infrastructure Failures Under One Roof
Each failure was independent, but they compounded. Fixing any one of them without the others would not have stabilised the product.
Backend Cannot Scale Past 100 Calls/Day
Technical Root Cause
Firebase Realtime Database + Cloud Functions with no queue, no worker pool, no connection pooling. Every call triggered a synchronous Cloud Function chain. Under concurrent load, function cold-start times compounded and the DB hit its connection limit.
Business Impact
Three beta customers generating ~100 calls/day. Series A target required 25 customers — roughly 25× the load. No investor would fund a company whose infrastructure demonstrably could not reach its own growth targets.
Why a Patch Would Not Work
No queue system meant every spike was a direct hit on the database. Cloud Functions are stateless and cold-start under load. Firebase is not designed for high-throughput streaming workloads.
No Real-Time Processing — 5-Minute Batch Delay
Technical Root Cause
Call transcription and analysis ran as a scheduled Cloud Scheduler job every 5 minutes. Audio files were uploaded to GCS, then a batch job pulled them, called the transcription API, and wrote results back. The pipeline had no streaming component.
Business Impact
Home service calls include emergencies — a burst pipe, a gas leak, an HVAC failure in winter. A 5-minute delay before the system detects the word "emergency" and escalates to a human CSR is operationally unacceptable. Two enterprise prospects raised this as a deal-breaker.
Why a Patch Would Not Work
Batch was chosen for simplicity during prototype. There was no architectural path from scheduled batch to real-time streaming without a rebuild — the data model and trigger logic were both batch-native.
CRM Sync Failing — 30% of Bookings Lost
Technical Root Cause
The CRM integration was a hand-written REST client with no retry logic, no idempotency, and no error handling beyond a try/catch that swallowed failures silently. Transient API errors (rate limits, timeouts, 5xx) caused permanent data loss — jobs booked by the AI never appeared in the CRM.
Business Impact
30% of AI-booked jobs were not appearing in the CRM. Customers discovered this by manually cross-referencing call logs and job boards. Two of three beta customers were threatening to cancel. One had already begun evaluating alternatives.
Why a Patch Would Not Work
Idempotency was never designed in — each booking request had no deduplication key, so retry attempts created duplicate jobs. The fix required rearchitecting the sync layer, not patching the client.
Zero Analytics — No Way to Measure AI Performance
Technical Root Cause
No call scoring, no booking rate tracking, no CSR performance metrics, no comparison between AI-handled and human-handled calls. The system wrote call transcripts to Firebase but had no query layer, no aggregation, and no reporting pipeline.
Business Impact
The primary sales objection in every enterprise demo: "How do I know the AI is actually working?" Without data, the answer was effectively "trust us." This blocked every deal above $20k ACV. It also meant the team had no signal for improving the AI — prompt changes were shipped blind.
Why a Patch Would Not Work
Analytics was deprioritised during prototype phase. Firebase's query limitations (no joins, no aggregations) meant a proper analytics layer required a separate data store from the start.
Why Hiring Was Not an Option at This Stage
Realistic hiring timelines for the roles needed: Senior Backend Engineer (4–6 months), ML Engineer for real-time systems (6–9 months), DevOps / Platform Engineer (3–4 months). Combined: 12–18 months and €300k–€450k in annual salaries, before any productivity.
The company had approximately €150k in the bank at a €40k/month burn rate. Series A was 4 months out, requiring 25 customers and demonstrable ARR traction. There was no intersection between what hiring could deliver and what the fundraise required.
The decision was to engage a specialist engineering team on a fixed-scope, fixed-price basis: build the production infrastructure in 12 weeks, hand it over with full documentation and test coverage, and let the founding team operate it independently thereafter.
The total engagement cost was €60,000 across four phases. The infrastructure savings alone (reduced Firebase and AI costs) recovered that cost within 8 months of deployment.
12 Weeks Across Four Phases
Each phase overlapped with the next to maximise parallel progress. Every phase shipped a production-usable increment — not internal work in progress. The client's beta customers were on the new infrastructure by week 5.
Phase 1 · Weeks 1–4
Real-Time Call Processing Pipeline
Replaced the Firebase + Cloud Functions batch architecture with an event-driven AWS pipeline capable of handling 25,000+ concurrent calls with sub-500ms transcription latency.
Key Engineering Decisions
SQS over Kafka
Volume at launch did not justify Kafka's operational overhead. SQS with FIFO queues provided the ordering guarantees needed for call processing with far simpler ops. Kafka was noted as the migration path if volume exceeded 50k calls/day.
PostgreSQL over DynamoDB
Call analytics requires multi-table joins, aggregations, and window functions — all of which DynamoDB handles poorly. PostgreSQL with read replicas provided the query flexibility needed without sacrificing write throughput at the call volumes projected.
Lambda for ingestion, ECS Fargate for workers
Call ingestion is bursty and short-lived — Lambda is the right fit. Call processing workers are long-running and stateful — Fargate with persistent Redis connections is more appropriate. Using both allowed right-sizing each component independently.
What Was Built
- Twilio SIP → Lambda ingestion with caller validation and intelligent routing
- Amazon Transcribe streaming: audio chunks → text in <500ms
- Redis Pub/Sub for event distribution between pipeline stages
- AWS Comprehend for real-time sentiment scoring on every utterance
- Emergency keyword + tone pattern detection with SMS escalation in <15s
- Amazon SQS FIFO queues with Node.js worker pool (parallel processing)
- S3 recording archival with CloudFront CDN distribution
- 99.9% uptime SLA, load tested at 25,000 concurrent simulated calls
Phase 2 · Weeks 3–6
Idempotent CRM Sync Engine
Rebuilt the brittle REST client into a production sync engine with idempotency guarantees, exponential backoff, conflict resolution, and full audit logging.
Key Engineering Decisions
Redis for idempotency keys, not the primary DB
Idempotency key lookups need to be fast (in the hot path of every booking) and ephemeral (24-hour TTL is sufficient). A Redis SET with SETNX is a single sub-millisecond operation. Adding this to PostgreSQL would have required an additional table write on every call.
CRM as source of truth on conflicts
When the platform and the CRM disagree on a job's state, the CRM wins. This is the only safe choice — the CRM holds data from other sources (field technicians, dispatch, manual entry) that the platform cannot see. Attempting to merge would corrupt data.
Webhook receiver over polling
Polling the CRM for updates introduces latency proportional to poll interval and generates unnecessary API calls. The CRM supports outbound webhooks — using them dropped the average update latency from minutes to seconds and eliminated 90% of outbound API traffic.
What Was Built
- Webhook receiver: CRM → platform (job status, customer updates, technician availability)
- API client: platform → CRM (book jobs, update customer records, attach call transcripts)
- Redis-backed idempotency keys (SETNX) — zero duplicate job creation guaranteed
- Exponential backoff with jitter: 3 retries over 15 minutes on transient failures
- Conflict resolution: CRM is source of truth; all merge decisions logged
- Per-customer field mapping layer with schema validation
- <5 second booking-to-CRM latency at p99 (from 5 minutes)
- Admin dashboard: per-customer sync status, failure log, retry queue depth
Phase 3 · Weeks 5–10
Real-Time Scoring & Coaching System
Built the data layer that turned "trust us" into auditable proof: a multi-dimensional call scoring engine, manager dashboards, and automated CSR coaching.
Key Engineering Decisions
NLP scoring in-process, not a separate microservice
Adding a scoring microservice would have introduced network latency and an additional failure surface in the hot path. The scoring logic was stateless and CPU-light enough to run inside the existing call processor worker without measurable latency impact.
GPT-4 only for technical accuracy — Comprehend for everything else
Sentiment, greeting detection, and keyword matching do not require a large language model. AWS Comprehend handles these at a fraction of the cost and latency. GPT-4 was reserved for the one dimension where reasoning over transcript content was genuinely required: checking whether a CSR gave technically accurate advice about home systems.
Materialized views for the manager dashboard
The manager dashboard queries booking rates, call volumes, and CSR scores across time ranges. Running these queries on the raw call table would have been expensive and slow at scale. PostgreSQL materialized views, refreshed every 5 minutes, kept dashboard queries under 50ms.
What Was Built
- Five-dimension scoring: greeting, objection handling, closing technique, empathy, technical accuracy
- Conversation flow analysis: qualifying questions, pricing clarity, upsell attempt, appointment confirmation
- Outcome tracking: job booked vs. not, revenue per call, post-call survey
- AI vs. human CSR performance comparison with per-call attribution
- Real-time manager dashboard: live call feed, team leaderboard, booking rate by hour
- Individual CSR scorecards with 7-day and 30-day trend lines
- Missed opportunity alerts: calls where booking was likely but not attempted
- Auto-coaching summaries delivered to CSR via Slack within 60 seconds of call end
Phase 4 · Weeks 9–12
Performance Optimisation & Handover
Query tuning, AI cost reduction, infrastructure hardening, and complete documentation — leaving the team with a system they could own and operate independently.
Key Engineering Decisions
Monthly table partitioning for call data from the start
Call records are append-only and queried almost exclusively by date range. Partitioning by month meant each partition was a separate physical table, allowing PostgreSQL to prune irrelevant partitions entirely from range queries. At 800k calls/month, this was not premature — it was necessary.
Prompt caching before model routing
We implemented prompt caching first because the gains were larger and simpler. Caching the system prompt across calls reduced tokens by 35% immediately. Model routing required more careful validation to ensure quality was preserved — it came second and added another 25% reduction on top.
PgBouncer in transaction mode, not session mode
Session mode holds a database connection for the full client session. With Lambda functions making short-lived calls, session mode would have held connections idle for most of their lifetime. Transaction mode recycles connections after each transaction, allowing 1,000 Lambda connections to share 20 actual database connections.
What Was Built
- Query indexing on call_id, customer_id, created_at, booking_status: lookup time 8s → 50ms
- Monthly table partitioning on calls table for time-range query pruning
- PostgreSQL read replicas: analytics queries separated from transactional write path
- PgBouncer in transaction mode: 1,000 → 20 active database connections
- OpenAI prompt caching: 35% token reduction; model routing (GPT-4 / GPT-3.5): further 25% reduction
- Combined AI cost reduction: $15,000 → $6,000/month (−60%)
- Multi-region failover: primary US-East-1, standby US-West-2 with automated failover
- Load tested at 25,000 concurrent simulated calls before handover
How the Core Systems Work
Two systems required careful design to get right under production conditions: the CRM sync (where bugs meant permanently lost bookings) and the call scoring engine (where latency meant a worse product). Here is the actual implementation approach for both.
End-to-End Call Pipeline
CRM Sync — Idempotent Booking
The core invariant: a call can only create one CRM job, regardless of how many times the sync is retried. Redis SETNX provides an atomic check-and-set. On failure, the lock is released so the retry queue can acquire it and try again.
// Idempotent CRM booking sync
async function syncBookingToCRM(call: ProcessedCall) {
const idempotencyKey = `booking:${call.id}:${call.customerId}`;
// Atomic check-and-set — prevents duplicate jobs
const acquired = await redis.set(
idempotencyKey, 'pending',
{ NX: true, EX: 86400 } // 24-hour TTL
);
if (!acquired) {
const jobId = await redis.get(idempotencyKey);
return { status: 'already_synced', jobId };
}
try {
const job = await crmClient.jobs.create({
customerId: call.customerId,
jobType: call.detectedJobType,
scheduledDate: call.preferredDate,
address: call.serviceAddress,
description: call.transcriptSummary,
priority: call.isEmergency ? 'URGENT' : 'NORMAL',
source: 'ai-voice-platform',
});
await redis.set(idempotencyKey, job.id, { EX: 86400 });
await db.bookingSyncs.create({
callId: call.id, crmJobId: job.id,
syncedAt: new Date(), status: 'success',
});
return { status: 'synced', jobId: job.id };
} catch (err) {
await redis.del(idempotencyKey); // Release lock for retry
await retryQueue.add(
{ call, attempt: 1 },
{ backoff: { type: 'exponential', delay: 2000 }, attempts: 5 }
);
throw err;
}
}Call Scoring — Parallel Dimension Analysis
Sentiment and GPT-4 fact-check run in parallel — no sequential bottleneck. Comprehend handles everything latency-sensitive; GPT-4 is invoked only for technical accuracy, where it is genuinely needed.
// Multi-dimension call quality scoring
async function scoreCall(transcript: string): Promise<CallScore> {
// Dimensions scored in parallel — no sequential bottleneck
const [sentiment, factCheck] = await Promise.all([
comprehend.detectSentiment({ Text: transcript, LanguageCode: 'en' }),
openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: TECHNICAL_ACCURACY_PROMPT },
{ role: 'user', content: transcript },
],
}),
]);
const scores = {
greeting: scoreGreeting(transcript), // keyword match
objectionHandling: scoreObjections(transcript), // ratio: raised / handled
closing: scoreClosing(transcript), // scheduling intent detection
empathy: scoreEmpathy(sentiment), // Comprehend positive score
accuracy: parseFloat( // GPT-4 structured output
factCheck.choices[0].message.content ?? '0'
),
};
// Weighted composite — weights reflect booking impact analysis
const overall =
scores.greeting * 0.15 +
scores.objectionHandling * 0.30 +
scores.closing * 0.25 +
scores.empathy * 0.15 +
scores.accuracy * 0.15;
return { overall: Math.round(overall), breakdown: scores };
}Full Deliverables at Handover (Week 12)
API Surface
- ›/api/calls — inbound routing
- ›/api/transcriptions
- ›/api/bookings — job logic
- ›/api/crm-sync — integration
- ›/api/analytics — reporting
- ›/api/coaching — scoring
- ›/api/admin — management
Background Workers
- ›call-processor (transcription + analysis)
- ›crm-sync (booking + updates)
- ›analytics-aggregator (daily rollups)
- ›cost-optimizer (model routing)
- ›alert-manager (escalation)
Test Coverage
- ›850+ unit tests (85% coverage)
- ›120+ integration tests
- ›15+ load tests (10k+ concurrent)
- ›30+ E2E: call → booking → CRM
Documentation
- ›OpenAPI spec (all endpoints)
- ›Architecture diagram (Lucidchart)
- ›15 incident response runbooks
- ›Terraform for all AWS resources
- ›DataDog dashboard templates
- ›Security audit report (pen test)
Sprint Timeline
Phases overlapped to maintain momentum. Beta customers were on the new pipeline by week 5.
Pipeline Live
Beta customers migrated to new infrastructure
- ›AWS Lambda call ingestion deployed
- ›Transcribe streaming integrated
- ›Redis Pub/Sub event bus operational
- ›SQS queue + worker pool live
- ›Load tested at 10k concurrent
CRM Sync Stable
99.5% sync success rate achieved, churn risk resolved
- ›Idempotent booking sync shipped
- ›Webhook receiver from CRM live
- ›Retry queue with backoff deployed
- ›Admin sync dashboard live
- ›Customer churn threat resolved
Analytics Live
Booking rate visible; enterprise sales unblocked
- ›5-dimension scoring engine
- ›Manager coaching dashboard
- ›CSR scorecards and trend data
- ›Auto-coaching via Slack
- ›Enterprise "prove it" objection resolved
Optimised & Handed Over
AI cost −60%; 25k-call load test passed; full docs
- ›Query optimisation complete (8s → 50ms)
- ›Prompt caching + model routing
- ›Multi-region failover configured
- ›25,000-call load test passed
- ›Full documentation + runbook handover
System Performance — Before and After
Week 12 infrastructure metrics and the 6-month business trajectory that followed. All figures are from the client — not modelled.
| Metric | Before | After (Week 12) | Change |
|---|---|---|---|
| Call Capacity | 100 calls/day | 25,000 calls/day | 250× |
| Call → CRM Latency | 5 minutes (batch) | <5 seconds (real-time) | 60× faster |
| Uptime | 94% (weekly crashes) | 99.9% | +5.9pp |
| CRM Sync Success Rate | 70% | 99.5% | +29.5pp |
| AI Model Cost | $15,000/month | $6,000/month | −60% |
| Total Infrastructure Cost | $19,000/month | $12,500/month | −34% |
| Booking Rate (customer avg) | 55% | 85% | +30pp |
| DB Query Latency (p50) | 8 seconds | 50ms | −99% |
Customer Growth (6 months)
- Month 1: 12 customers (from 3 at handover)
- Month 3: 25 customers (Series A target)
- Month 6: 100+ customers across 6 verticals
- Churn rate post-launch: 0%
- NPS: 20 → 75 (detractor to promoter)
Revenue Trajectory
- MRR month 1: $30k → $180k (6×)
- MRR month 6: $500k
- ARR run-rate month 6: $6M
- Gross margin: 35% → 72% (AI cost savings)
- ACV: $10k → $15k (new pricing tier unlocked)
Platform Scale (6 months)
- Month 1: 50,000 calls processed
- Month 3: 200,000 calls/month
- Month 6: 800,000 calls/month
- Total calls handled: 2.5M+
- Jobs booked via platform: 680,000
What Customers Said After Deployment
“I can run a $100M business with 9 CSRs because the platform handles 70% of our entire call volume — all while booking at a higher rate than ever before.”
— President, multi-location home services operator
“Before, our customers were waiting 4 to 5 minutes and sometimes weren't getting answered at all. Now if we don't answer it, it goes straight to the AI.”
— Director of Operations, plumbing services company
“Call quality went from 40% to 95%, booking rate sits at 85%, and we're averaging 400 calls weekly. We have the data to prove it now.”
— CEO, multi-location HVAC company
“Booking rate went from 55% to 90%. Our dispatch board got so full we had to hire more technicians.”
— Owner, franchise home services business
Cost of the Engagement vs. Alternatives
The fixed-price engagement was €60,000. Infrastructure cost savings alone recovered this within 8 months. The fundraising outcome made the comparison academic — but the numbers are worth documenting.
Option A — Build an Internal Team
Senior Backend + ML + DevOps (realistic 2024 European market rates)
Option B — Kuvaka Fixed Sprint
12-week fixed-price engagement, four phases
The Series A Outcome
The Raise
The client closed a $18M Series A round 4 months after the infrastructure handover. The round was led by a top-tier VC firm. The pitch was built substantially on the production metrics the new infrastructure made possible: uptime, booking rates, CRM reliability, and calls-per-month growth.
What Changed the Narrative
Pre-Kuvaka, the company had 3 customers and a prototype. Post-Kuvaka, it had 25 customers, demonstrable infrastructure that had handled millions of calls, and unit economics that worked. The coaching analytics turned customer testimonials from anecdotes into dashboards.
Where It Went
The infrastructure built in the 12-week sprint served the company through its next 12 months of growth without a major rebuild. By month 18, the company was processing 5M calls/month on the same architectural foundation, with their own team extending it.
Stack Used in This Engagement
Backend
- ›Node.js 20 (TypeScript)
- ›PostgreSQL 16 (AWS RDS Multi-AZ)
- ›Redis 7 (AWS ElastiCache)
- ›PgBouncer — transaction mode pooling
AI & Voice
- ›OpenAI GPT-4 + GPT-3.5 (routed)
- ›Amazon Transcribe (streaming)
- ›AWS Comprehend (sentiment + NER)
- ›Twilio Voice API (SIP/PSTN)
Processing
- ›Amazon SQS FIFO (job queue)
- ›AWS Lambda (stateless ingestion)
- ›Amazon ECS Fargate (stateful workers)
- ›Redis Pub/Sub (event bus)
Infrastructure
- ›Terraform (all AWS resources as code)
- ›Docker + Kubernetes EKS (workers)
- ›GitHub Actions (CI/CD pipeline)
- ›US-East-1 primary + US-West-2 standby
Observability
- ›DataDog APM + custom dashboards
- ›Sentry (error tracking + grouping)
- ›AWS CloudWatch (infrastructure logs)
- ›PagerDuty (on-call alerting)
Integrations
- ›Home services CRM (two-way sync)
- ›Stripe (subscription billing)
- ›Slack (coaching + alert delivery)
- ›AWS S3 + CloudFront (call recordings)
What We Would Do the Same and Differently
What Worked Well
Design for 10× the current load from day one
Building the pipeline for 25,000 calls/day when the client had 100 added negligible cost but avoided a full rebuild at exactly the wrong moment — 3 months into Series A traction. Every architectural decision was made with growth already in the model.
Idempotency as a first-class constraint, not an afterthought
Designing the CRM sync around idempotency keys from the start meant retries were safe from day one. Retrofitting idempotency onto an existing system that has already produced duplicate records is considerably harder.
Real-time over batch everywhere it mattered
The 5-minute batch delay was not a performance problem — it was a product problem. Emergency escalation, coaching feedback, and booking confirmation all become fundamentally better features when they happen in seconds rather than minutes.
Analytics as a product feature, not internal tooling
The scoring dashboard was the primary reason 5 enterprise deals closed. It turned "the AI is working" from a claim into verifiable data. Building it as a customer-facing product rather than an internal report was the right framing.
What We Would Do Earlier
Start with Kubernetes for the worker layer
The workers were initially deployed as Lambda functions, then migrated to ECS Fargate in Phase 4. Long-running workers with persistent Redis connections and stateful processing are not a good fit for Lambda. Starting with ECS Fargate would have saved the migration time.
Prompt caching from the first API call
Prompt caching was added in week 8 as an optimisation. It reduced OpenAI spend by 35% immediately. There was no technical reason it could not have been in place from week 1 — it should be the default on any LLM integration.
Per-customer data isolation earlier
Multi-tenancy isolation at the database row level was designed in from the start, but schema-level isolation (separate schemas per customer) was added in week 10 for the enterprise tier. Adding it earlier would have simplified the SOC 2 audit scope considerably.
Load testing at the architecture stage, not just at handover
The final load test at 25,000 concurrent calls validated the system but came at the end. Earlier load tests (even at 1,000 and 5,000 concurrent calls) at the architecture stage would have caught one connection pooling issue 4 weeks earlier than we found it.
Where the Platform Went From Here
The Kuvaka-built infrastructure served the client through its next 18 months of growth. The founding team hired engineers into a stable, well-documented system rather than inheriting a prototype they did not understand.
Months 6–12
- Customers: 100 → 500
- Revenue: $6M → $30M ARR
- Team: 15 → 85 employees
- Monthly calls: 800k → 5M
- Industries served: 6 → 12
Product Expansion
- Outbound campaigns (re-engagement)
- Google Local Services Ads integration
- Web chat for website visitors
- Mobile app for field technicians
- Spanish and French language support
Month 18+
- Series B fundraising completed
- 1,000+ customers across all verticals
- $100M+ ARR
- Certified Partner with 3 major CRMs
- SOC 2 Type II certification achieved
“We tried to hire for 4 months and got nowhere. Kuvaka delivered in 12 weeks what would have taken us 18+ months to build ourselves. The CRM integration alone saved our company — we went from two of three customers about to churn to 100% retention and 10× growth in 6 months. If you're a technical founder drowning in infrastructure debt, call Kuvaka.”
— CEO & Co-Founder (anonymised at request)
Need Production Infrastructure Before Your Next Raise?
If your AI product is on a prototype foundation and you have a funding or customer deadline, submit a technical diagnostic. We will scope the engagement and return a fixed-price proposal within 24 hours.
Scoped Diagnostic
Technical review of your current architecture with an honest assessment of what needs to change and in what order.
Fixed-Price Delivery
One price agreed before we start. Delivered in 8–12 weeks with tests, documentation, and handover included.
You Own the Output
Full ownership of all code, infrastructure, and documentation from day one. No lock-in.
No retainer. Fixed scope. You own all deliverables.