Project Decision Records¶
This document captures key project-level decisions including roadmap priorities, feature decisions, and implementation strategies.
What is a PDR?¶
A Project Decision Record (PDR) documents significant project decisions that impact scope, timeline, priorities, or business direction. Unlike ADRs which focus on technical architecture, PDRs capture strategic and tactical project decisions.
PDR-001: FastAPI Backend Roadmap¶
Status¶
Accepted
Date¶
2025-12-24
Context¶
Need to define the implementation roadmap for migrating from direct Odoo access to FastAPI control plane architecture (see ADR-012, ADR-013, ADR-014).
Decision¶
Implement in 9-week phases with the following priority order:
Phase 1: FastAPI Core (Week 1-2)¶
- Project structure with FastAPI
- Database schema + Alembic migrations
- Core infrastructure:
- CORS, rate limiting, correlation IDs
- Structured logging (structlog)
- Health endpoints (/health, /health/ready, /health/live)
- Odoo client with:
- Connection pooling
- Circuit breaker
- Retry with exponential backoff
- Basic tests
Phase 2: Auth Migration (Week 3)¶
- JWT authentication endpoints
- User model + password hashing
- User migration script (Odoo → FastAPI)
- Password reset flow with email
- PWA login updates
Phase 3: FSM Job Endpoints (Week 4)¶
- /jobs endpoints (tenant-scoped)
- FSM order CRUD operations
- Job completion workflow
- PWA job module updates
Phase 4: CRM & Sales (Week 5)¶
- /crm endpoints (leads, opportunities)
- /sales endpoints (quotes, orders)
- Landing page form integration
Phase 5: Portal & Tokens (Week 6)¶
- Customer portal tokens
- Scoped, expiring access
- Self-service job status
- Quote approval workflow
Phase 6: Email & SMS (Week 7)¶
- Message outbox pattern
- AWS SES integration
- JustCall SMS integration
- Background worker for delivery
Phase 7: Audit & Multi-Tenant (Week 8)¶
- Complete audit logging
- Tenant model + membership
- OdooRouting table
- TenantMiddleware
- OdooProvisioner (auto DB creation)
Phase 8: Monitoring & Production (Week 9)¶
- Prometheus metrics
- Grafana dashboards
- Documentation + runbooks
- Production deployment
- Load testing (optional)
Rationale¶
- Auth first: Foundation for all other features
- FSM next: Highest-value PWA feature
- Portal later: Depends on auth + business endpoints
- Multi-tenant last: Can retrofit after single-tenant works
Consequences¶
- PWA changes spread across multiple phases
- Need parallel development tracks
- Some temporary direct Odoo access during migration
PDR-002: Multi-Tenant Pricing Strategy¶
Status¶
Proposed
Date¶
2025-12-24
Context¶
With multi-tenant SaaS architecture decided (ADR-013), need to define subscription plans and pricing.
Decision¶
Four-tier subscription model:
| Plan | Monthly | Yearly | Target |
|---|---|---|---|
| Free | $0 | $0 | Trials, small ops |
| Starter | $49 | $470 | Growing businesses |
| Professional | $99 | $950 | Established ops |
| Enterprise | $299 | $2,870 | Large operations |
Feature Matrix¶
| Feature | Free | Starter | Pro | Enterprise |
|---|---|---|---|---|
| Users | 2 | 5 | 15 | Unlimited |
| Technicians | 1 | 3 | 10 | Unlimited |
| Jobs/month | 20 | 100 | 500 | Unlimited |
| PDF Reports | Yes | Yes | Yes | Yes |
| Custom Branding | No | Yes | Yes | Yes |
| API Access | No | No | Yes | Yes |
| Advanced Reports | No | No | Yes | Yes |
| Multi-Location | No | No | Yes | Yes |
| Recurring Jobs | No | Yes | Yes | Yes |
| Inventory | No | No | Yes | Yes |
Rationale¶
- Free tier for acquisition and trials
- Starter priced for small operations ($49 accessible)
- Professional unlocks full features
- Enterprise for high-volume customers
Consequences¶
- Need Stripe integration
- Feature flag system required
- Limit enforcement in FastAPI
- Upgrade prompts in PWA/Portal
PDR-003: Template Database Strategy¶
Status¶
Accepted
Date¶
2025-12-24
Context¶
For multi-tenant provisioning (ADR-014), need a strategy for creating new Odoo databases for tenants.
Decision¶
Use template database duplication approach:
- Maintain
template_jdxdatabase with: - All required modules installed
- Default configuration
- Sample data removed
-
Admin user for API access
-
Provisioning flow:
-
Template updates:
- Test changes on template database
- Version template database in backups
- Document module list in template
Alternatives Considered¶
- Fresh install per tenant: Too slow (5-10 min vs 30 sec)
- Shared multi-company: Insufficient isolation
- Pre-created pool: Complexity, resource waste
Rationale¶
- Duplication is fast (~30 seconds)
- Consistent configuration across tenants
- Easy to update (change template, new tenants get updates)
- Odoo-native operation
Consequences¶
- Must maintain template database
- Template updates don't affect existing tenants
- Need process for applying updates to existing tenants
- Storage for template backups
PDR-004: Frontend Migration to Next.js¶
Status¶
Accepted (Updated)
Date¶
2025-12-24
Context¶
The Flask-based PWA and Landing Page are being replaced with a unified Next.js frontend (see ADR-015). This PDR outlines the migration strategy from Flask to Next.js while integrating with the FastAPI control plane.
Decision¶
Complete frontend rewrite with Next.js, migrating in phases:
Phase 1: Next.js Foundation (Week 1)¶
- Create
nextjs-frontend/project structure - Configure TypeScript, Tailwind CSS 4, ESLint
- Setup Docker container for development and production
- Configure NextAuth.js for JWT auth with FastAPI
- Create shared API client for FastAPI communication
- Basic layout components and routing structure
Phase 2: Landing Page Migration (Week 2)¶
- Port marketing pages from Flask Jinja templates to React components
- Implement contact/lead form with FastAPI integration
- Setup SEO: metadata, sitemap.xml, robots.txt
- Configure SSG for static pages, ISR for dynamic content
- Add analytics and reCAPTCHA integration
Phase 3: Field PWA Core (Week 3-4)¶
- Port job list and job detail views
- Implement job completion flow with signature capture
- Camera integration for photo uploads
- Setup service worker with Serwist for offline support
- Push notification configuration
- Install prompt and PWA manifest
Phase 4: Customer Portal (Week 5)¶
- Token-based quote viewing
- Digital signature for quote approval
- Job status tracking for customers
- Email notification integration
Phase 5: Decommission Flask Apps (Week 6)¶
- Redirect all routes to Next.js
- Remove Flask PWA container
- Remove Landing Page container
- Update nginx configuration
- Archive Flask codebases
Technical Implementation¶
// API Client pattern for FastAPI
import { createApiClient } from '@/lib/api';
const api = createApiClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL,
getToken: () => getSession()?.accessToken,
});
// React Query for data fetching
const { data: jobs } = useQuery({
queryKey: ['jobs'],
queryFn: () => api.get('/jobs'),
});
Routing Strategy¶
| Old Flask Route | New Next.js Route | Notes |
|---|---|---|
Landing / |
/ |
SSG |
Landing /contact |
/contact |
With form |
PWA /jobs |
/field/jobs |
Protected, JWT |
PWA /jobs/<id> |
/field/jobs/[id] |
Protected |
Portal /quote/<token> |
/portal/quote/[token] |
Token auth |
Rollback Plan¶
- Keep Flask containers available but inactive
- Nginx can switch routing instantly
- Feature flags in Next.js for partial rollback:
Rationale¶
- Clean break: New codebase avoids legacy complexity
- Modern stack: React 19, TypeScript, better DX
- Unified frontend: One codebase instead of two Flask apps
- Better offline: Next.js + Serwist for robust PWA
- SEO optimized: SSG/SSR for marketing pages
Consequences¶
- Parallel development during migration (Flask + Next.js)
- Need to maintain Flask apps until migration complete
- Learning curve for team new to React/Next.js
- Better long-term maintainability
- Single codebase reduces operational overhead
PDR-005: Documentation as Code¶
Status¶
Accepted
Date¶
2025-12-24
Context¶
FastAPI architecture introduces significant documentation needs. Need to ensure documentation stays current.
Decision¶
Documentation as Code approach:
- All docs in Git (docs/ folder)
- MkDocs Material for rendering
- PR reviews include docs
- Auto-generated API docs from FastAPI OpenAPI
Documentation Structure¶
docs/content/
├── development/
│ ├── adr.md # Architecture decisions
│ ├── pdr.md # Project decisions (this file)
│ └── fastapi/ # NEW: FastAPI-specific docs
│ ├── index.md
│ ├── authentication.md
│ ├── multi-tenancy.md
│ └── deployment.md
├── api/
│ ├── index.md # Existing Odoo API
│ └── fastapi/ # NEW: FastAPI API docs
│ ├── index.md
│ ├── auth.md
│ ├── jobs.md
│ └── ...
└── operations/
└── fastapi/ # NEW: FastAPI operations
├── deployment.md
├── monitoring.md
└── troubleshooting.md
Rationale¶
- Version control for docs
- PR process ensures reviews
- Same deployment as code
- OpenAPI auto-generation reduces manual work
Consequences¶
- Docs PRs alongside code PRs
- Build docs in CI/CD
- Need doc contribution guidelines
- Review process may slow PRs
PDR-006: Multi-Tenant Frontend Architecture (v2)¶
Status¶
Proposed
Date¶
2025-12-24
Context¶
With multi-tenant SaaS (ADR-013) and Next.js frontend (ADR-015), each tenant company needs their own customer-facing website for: - Landing page with company branding - Contact/lead forms - Helpdesk ticket submission - Customer portal (quote viewing, job status)
Different tenant domains (e.g., jdx-austin.com, blinds-dallas.com) should all be served by a single Next.js application with tenant-specific branding and data isolation.
Decision¶
Implement v2: Multi-Tenant Frontend after v1 (single-tenant Next.js) is complete.
Architecture Overview¶
┌─────────────────────────────────────────────────────────────────────┐
│ Tenant Domains │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ jdx-austin │ │ blinds-dallas │ │ shutters-houston│ │
│ │ .platform.com │ │ .platform.com │ │ .platform.com │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ (서브도메인) │ │ │
│ └──────────────────────┼─────────────────────┘ │
│ │ │
│ ┌─────────────────┐ │ │
│ │ www.jdx.com │ ───────────┤ (커스텀 도메인 - Premium) │
│ └─────────────────┘ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Next.js Frontend │ │
│ │ ┌────────────────────┐ │ │
│ │ │ Middleware │ │ │
│ │ │ (도메인→테넌트 해석) │ │ │
│ │ └────────────────────┘ │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ FastAPI Control Plane │ │
│ │ - Tenant validation │ │
│ │ - API routing │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ odoo_jdx │ │ odoo_dallas │ │ odoo_houston │ │
│ │ (Database) │ │ (Database) │ │ (Database) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Phase v2.1: Tenant Domain System (Week 1-2)¶
FastAPI Changes:
# New table: tenant_domains
class TenantDomain(Base):
__tablename__ = "tenant_domains"
id = Column(UUID, primary_key=True)
tenant_id = Column(UUID, ForeignKey("tenants.id"))
domain_type = Column(Enum("subdomain", "custom"))
domain = Column(String(255), unique=True) # "jdx-austin" or "www.jdx.com"
is_primary = Column(Boolean, default=False)
ssl_status = Column(Enum("pending", "active", "failed"))
created_at = Column(DateTime)
# API endpoints
GET /api/tenants/by-domain?domain=xxx
POST /api/tenants/{id}/domains
DELETE /api/tenants/{id}/domains/{domain_id}
Next.js Middleware:
// middleware.ts
export async function middleware(request: NextRequest) {
const host = request.headers.get('host');
// 1. Subdomain detection: xxx.platform.com
// 2. Custom domain lookup: www.company.com → tenant
// 3. Inject tenant context into request
const tenant = await resolveTenant(host);
if (!tenant) return NextResponse.redirect('/not-found');
// Pass tenant to server components via headers
const response = NextResponse.next();
response.headers.set('x-tenant-id', tenant.id);
response.headers.set('x-tenant-slug', tenant.slug);
return response;
}
Phase v2.2: Tenant Branding System (Week 3)¶
Tenant Settings Table:
class TenantSettings(Base):
__tablename__ = "tenant_settings"
tenant_id = Column(UUID, ForeignKey("tenants.id"), primary_key=True)
# Branding
company_name = Column(String(100))
logo_url = Column(String(255))
favicon_url = Column(String(255))
primary_color = Column(String(7)) # "#1e40af"
secondary_color = Column(String(7))
# Contact Info
phone = Column(String(20))
email = Column(String(100))
address = Column(Text)
# Social Links
social_facebook = Column(String(255))
social_instagram = Column(String(255))
# SEO
meta_title = Column(String(100))
meta_description = Column(String(255))
# Features
helpdesk_enabled = Column(Boolean, default=True)
portal_enabled = Column(Boolean, default=True)
Next.js Theme Provider:
// app/providers/TenantProvider.tsx
export function TenantProvider({ children, tenant }: Props) {
return (
<TenantContext.Provider value={tenant}>
<style>{`
:root {
--color-primary: ${tenant.primaryColor};
--color-secondary: ${tenant.secondaryColor};
}
`}</style>
{children}
</TenantContext.Provider>
);
}
Phase v2.3: Custom Domain SSL (Week 4)¶
Cloudflare for SaaS Approach:
1. 고객이 커스텀 도메인 등록 요청
2. FastAPI가 Cloudflare API로 도메인 추가
3. 고객에게 CNAME 설정 안내:
www.customer.com → tenants.platform.com
4. Cloudflare가 자동 SSL 발급
5. 도메인 상태 → "active"
Alternative: Let's Encrypt:
# Certbot integration for custom domains
async def provision_ssl(domain: str):
# 1. Verify domain ownership (DNS challenge)
# 2. Request certificate from Let's Encrypt
# 3. Store certificate
# 4. Update nginx config
pass
Phase v2.4: Tenant-Scoped Forms (Week 5)¶
All forms automatically include tenant context:
// Contact form - automatically scoped to tenant
async function submitContactForm(data: ContactFormData) {
const tenant = useTenant();
await api.post('/leads', {
...data,
tenant_id: tenant.id, // 자동 주입
source: 'website',
});
}
// Helpdesk ticket - automatically routed to tenant's Odoo
async function submitTicket(data: TicketData) {
const tenant = useTenant();
await api.post('/helpdesk/tickets', {
...data,
tenant_id: tenant.id,
});
// FastAPI routes to tenant's Odoo database
}
Phase v2.5: Admin Dashboard for Tenants (Week 6)¶
Tenant admin can configure their site:
/admin/settings
├── Branding (logo, colors, fonts)
├── Contact Info
├── Social Links
├── SEO Settings
├── Domain Management
│ ├── Primary domain
│ ├── Add custom domain
│ └── SSL status
└── Feature Toggles
Domain Strategy¶
| Tier | Domain Type | SSL | Example |
|---|---|---|---|
| Free | Subdomain only | Wildcard (*.platform.com) | demo.jdxplatform.com |
| Starter | Subdomain | Wildcard | company.jdxplatform.com |
| Pro | Subdomain + 1 Custom | Auto (Cloudflare/LE) | www.mycompany.com |
| Enterprise | Unlimited Custom | Auto | Multiple domains |
API Design for Multi-Tenant¶
All API endpoints are tenant-scoped:
# Every endpoint gets tenant from middleware
@router.post("/leads")
async def create_lead(
lead: LeadCreate,
tenant: Tenant = Depends(get_current_tenant)
):
# Automatically uses tenant's Odoo database
odoo = await get_odoo_client(tenant)
lead_id = await odoo.create("crm.lead", lead.dict())
return {"id": lead_id}
@router.post("/helpdesk/tickets")
async def create_ticket(
ticket: TicketCreate,
tenant: Tenant = Depends(get_current_tenant)
):
odoo = await get_odoo_client(tenant)
ticket_id = await odoo.create("helpdesk.ticket", {
**ticket.dict(),
"team_id": tenant.settings.helpdesk_team_id,
})
return {"id": ticket_id}
Development Phases Summary¶
| Phase | Scope | Depends On |
|---|---|---|
| v1 | Single-tenant Next.js | PDR-004 (Frontend Migration) |
| v2.1 | Tenant domain system | v1 complete, PDR-001 Phase 7 |
| v2.2 | Tenant branding | v2.1 |
| v2.3 | Custom domain SSL | v2.1 |
| v2.4 | Tenant-scoped forms | v2.2 |
| v2.5 | Tenant admin dashboard | v2.1-v2.4 |
Prerequisites (v1 Must Complete First)¶
Before v2 development: - [ ] PDR-004: Next.js frontend migration complete - [ ] PDR-001 Phase 7: Multi-tenant infrastructure in FastAPI - [ ] Tenant database and settings models - [ ] OdooRouting table functional
Alternatives Considered¶
| Option | Pros | Cons |
|---|---|---|
| Separate app per tenant | Full isolation | Doesn't scale, maintenance nightmare |
| Single domain with path routing | Simple | Poor branding (company.com/jdx) |
| Subdomain only | Easy SSL | Customers want own domain |
| Subdomain + Custom (chosen) | Flexible, scalable | SSL complexity |
Rationale¶
- Subdomain as default: Easy, immediate, no SSL hassle
- Custom domain as premium: Revenue opportunity, customer demand
- Single codebase: Maintainability, consistent features
- Cloudflare for SaaS: Industry standard for custom domain SSL
Consequences¶
- Need Cloudflare for SaaS or Let's Encrypt automation
- Middleware adds latency (tenant lookup per request)
- Need tenant context in all components
- Cache invalidation per tenant
- More complex local development (multiple domains)
PDR Template¶
Use this template for new PDRs:
## PDR-XXX: Title
### Status
Proposed | Accepted | Deprecated | Superseded
### Date
YYYY-MM-DD
### Context
What is the business/project need driving this decision?
### Decision
What is the decision being made?
### Alternatives Considered
What other options were evaluated?
### Rationale
Why was this decision made over alternatives?
### Consequences
What are the implications of this decision?
PDR-007: Email Service Provider Strategy¶
Status¶
Accepted
Date¶
2025-12-28
Context¶
With the FastAPI control plane architecture (ADR-012) and multi-tenant SaaS model (ADR-013), need a professional email service strategy for: - Transactional emails (order confirmations, invoices, portal magic links) - Customer portal token-based access (no signup required) - Per-tenant email domains (each tenant sends from their own domain) - High deliverability for critical emails
Decision¶
Adopt Hybrid Email Architecture with:
| Provider | Role | Use Cases |
|---|---|---|
| Postmark Pro | Primary | Magic links, order confirmations, invoices, password resets |
| AWS SES | Secondary/Fallback | Bulk emails, internal notifications, failover |
Postmark Pro Plan Details¶
- Cost: $16.50/month base (10K emails included)
- Extra emails: $1.30 per 1,000
- Signature domains: Up to 10 (one per tenant)
- Features: Fastest delivery, 45-day logs, excellent deliverability
Per-Tenant Domain Configuration¶
Each tenant has their own verified sending domain in Postmark:
Tenant A: orders@blinds-pro.com.au
Tenant B: orders@sydney-shutters.com.au
Tenant C: orders@outdoor-blinds.com.au
All managed via single Postmark account with: - Automated domain provisioning via Postmark Domains API - Per-tenant DNS instructions (SPF, DKIM, DMARC) - Tenant-tagged emails for metrics isolation
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ HYBRID EMAIL ARCHITECTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ POSTMARK PRO (Primary) │
│ ├── Customer-facing emails │
│ ├── Portal magic links (speed critical) │
│ ├── Order confirmations & invoices │
│ ├── Quote notifications │
│ └── Per-tenant sending domains │
│ │
│ AWS SES (Secondary) │
│ ├── Internal team notifications │
│ ├── Bulk status updates │
│ ├── System alerts │
│ └── Automatic failover from Postmark │
│ │
│ SHARED INFRASTRUCTURE │
│ ├── Unified suppression list │
│ ├── Bounce/complaint webhook handlers │
│ └── Email queue with provider routing │
│ │
└─────────────────────────────────────────────────────────────┘
Alternatives Considered¶
| Option | Pros | Cons |
|---|---|---|
| Postmark Only | Simple, excellent deliverability | No fallback, higher cost at scale |
| AWS SES Only | Lowest cost, already integrated | Complex setup, slower delivery, DIY deliverability |
| SendGrid | Good feature set | Deliverability issues on shared IPs |
| Hybrid (chosen) | Best of both, failover capability | Two providers to manage |
Rationale¶
- Speed: Postmark has industry-leading delivery times (critical for magic links)
- Deliverability: Postmark's sole focus on transactional email = best inbox rates
- Cost efficiency: SES for bulk/internal keeps costs down
- Resilience: Automatic failover if primary fails
- Multi-tenant: Postmark Pro supports 10 signature domains (tenants)
- Existing infrastructure: SES already configured from Phase 6 (ADR-012)
Implementation¶
Phase 1: Postmark Setup¶
- Create Postmark account
- Configure primary sending domain (jdx.com)
- Set up webhook endpoints for bounces/complaints
- Add Postmark client to FastAPI
Phase 2: Per-Tenant Domains¶
- Implement Postmark Domains API integration
- Add tenant email settings to
tenant.settingsJSONB - Create admin endpoints for domain management
- Automate DNS verification flow
Phase 3: Unified Email Service¶
- Create
UnifiedEmailServicewith provider routing - Implement suppression list (shared across providers)
- Add priority-based routing (critical → Postmark, bulk → SES)
- Automatic failover logic
Consequences¶
- Additional monthly cost ($16.50+ for Postmark Pro)
- Two providers to monitor and maintain
- DNS configuration required per tenant
- Better deliverability for customer-facing emails
- Reduced risk with failover capability
- Foundation for customer portal magic links
Related Decisions¶
- ADR-012: FastAPI Control Plane (email via FastAPI, not Odoo)
- ADR-016: Hybrid Email Architecture (technical details)
- PDR-001 Phase 6: Email & SMS integration
Decision Log¶
| ID | Title | Status | Date |
|---|---|---|---|
| PDR-001 | FastAPI Backend Roadmap | Accepted | 2025-12-24 |
| PDR-002 | Multi-Tenant Pricing Strategy | Proposed | 2025-12-24 |
| PDR-003 | Template Database Strategy | Accepted | 2025-12-24 |
| PDR-004 | Frontend Migration to Next.js (v1) | Accepted | 2025-12-24 |
| PDR-005 | Documentation as Code | Accepted | 2025-12-24 |
| PDR-006 | Multi-Tenant Frontend Architecture (v2) | Proposed | 2025-12-24 |
| PDR-007 | Email Service Provider Strategy | Accepted | 2025-12-28 |