Stories · Platform Spec
stories.victor.ponomarenko.io — Technical Requirements for Developers
→ See stories-vision.md for mission and creative context
→ See stories-creative-playbook.md for Oleksii's production guide
Authentication & Access (Non-Negotiable)
Stories is not public. It uses identical authentication to bio.victor — Lambda@Edge viewer-request + AWS Cognito + Google OAuth. The allowlist is managed independently.
| Parameter | Value |
|---|---|
| Auth mechanism | Lambda@Edge auth-check + parse-auth (same functions as bio.victor) |
| Cognito client | stories-client (aws_cognito_user_pool_client.stories) |
| Allowlist SSM | /story/prod/cognito/allowed-emails (separate from /victor-bio/cognito/allowed-emails) |
| Callback URL | https://stories.victor.ponomarenko.io/oauth2/callback |
| Logout URL | https://stories.victor.ponomarenko.io |
| Cognito domain | Shared: victor-bio.auth.eu-north-1.amazoncognito.com |
| Google JS origin | https://stories.victor.ponomarenko.io (already registered in Terraform) |
Access management: The allowlist SSM param is edited via AWS Console or CLI — no Terraform redeploy required. Adding a guest: aws ssm put-parameter --name /story/prod/cognito/allowed-emails --value "existing@emails.com,newguest@email.com" --overwrite.
The stories-client Terraform resource is already defined in infrastructure/terraform/cognito.tf. The Lambda@Edge functions read the SSM path from the COGNITO_CLIENT_ID_MAP environment variable, which must map stories-client-id → /story/prod/cognito/allowed-emails.
Infrastructure
AWS Resources
| Resource | Name / ID | Notes |
|---|---|---|
| S3 bucket | stories-victor-ponomarenko-io |
Private, SSE-S3, OAC only |
| CloudFront | CLOUDFRONT_STORIES_DISTRIBUTION_ID |
OAC origin, Lambda@Edge attached |
| ACM cert | stories.victor.ponomarenko.io |
us-east-1 (CloudFront requirement) |
| Route 53 | stories.victor.ponomarenko.io → CloudFront A alias |
Same hosted zone as bio.victor |
| Cognito client | stories-client |
Existing in cognito.tf |
| SSM allowlist | /story/prod/cognito/allowed-emails |
StringList, editable without redeploy |
| Lambda@Edge | Shared with bio.victor | auth-check + parse-auth + refresh-token |
Terraform Scope
Stories infra is co-managed in the same Terraform workspace as bio.victor. Required additions:
# CloudFront distribution for stories (mirrors bio.victor pattern)
resource "aws_cloudfront_distribution" "stories" {
# S3 OAC origin: stories-victor-ponomarenko-io
# Lambda@Edge: same auth-check + parse-auth functions
# Aliases: ["stories.victor.ponomarenko.io"]
# ACM cert: stories cert (us-east-1)
}
# Route 53 A alias record
resource "aws_route53_record" "stories" {
zone_id = data.aws_route53_zone.ponomarenko_io.zone_id
name = "stories.victor.ponomarenko.io"
type = "A"
alias { name = aws_cloudfront_distribution.stories.domain_name }
}
CI/CD Pipeline
Deploy Workflow: deploy-stories.yml
Trigger: push to main when slides/** or stories-site/** changes.
checkout
│
├── build step (if needed — Marp CLI, static gen)
│
├── aws s3 sync → s3://stories-victor-ponomarenko-io/
│ --delete --cache-control "max-age=300"
│
└── cloudfront create-invalidation
--distribution-id $CLOUDFRONT_STORIES_DISTRIBUTION_ID
--paths "/*"
GitHub Actions variables required:
| Variable | Value |
|---|---|
S3_STORIES_BUCKET_NAME |
stories-victor-ponomarenko-io |
CLOUDFRONT_STORIES_DISTRIBUTION_ID |
output from Terraform |
AWS_DEPLOY_ROLE_ARN |
Same OIDC role as bio.victor deploy |
Feature Flags & Canary Deployments
The platform must support controlled rollout without full redeployment.
Feature flags — CloudFront Function or Lambda@Edge reads SSM parameter /story/prod/flags/<flag-name> and injects a response header or cookie. The site JavaScript reads document.cookie or a <meta> tag to toggle features client-side.
SSM path convention:
/story/prod/flags/illustrations-enabled → "true" | "false"
/story/prod/flags/timeline-enabled → "true" | "false"
/story/prod/flags/new-homepage → "true" | "false" | "canary"
Canary deployments — two S3 path prefixes: stable/ and canary/. CloudFront origin-request Lambda@Edge routes a configurable % of requests (read from SSM /story/prod/canary/weight, e.g. "10") to canary/ prefix.
S3 layout:
stories-victor-ponomarenko-io/
stable/ ← 90% of traffic (default)
canary/ ← 10% of traffic (SSM-controlled weight)
A/B testing — same canary mechanism with ab-test cookie. First visit: Lambda@Edge assigns bucket A or B (stored in cookie). Subsequent visits: Lambda@Edge respects the cookie. Metrics collected via CloudFront access logs → Athena.
Cookie: stories-ab=A | stories-ab=B
SSM: /story/prod/ab-test/active → "true" | "false"
/story/prod/ab-test/b-weight → "0.5" (50/50 split)
All flag/canary/AB state changes take effect within 60 seconds (SSM read at Lambda@Edge cold start, cached in-memory per execution context). No redeploy needed for toggling.
Content Architecture
Content Types
| Type | Format | Source | Status |
|---|---|---|---|
| Slide decks (per chapter/part) | Marp HTML export | slides/ directory |
✅ Live |
| Illustrated chapter pages | Static HTML + CSS | stories-site/chapters/ |
Phase 2 |
| Artwork gallery | Static HTML, lazy-load JPG | stories-site/gallery/ |
Phase 2 |
| Interactive timeline | Vanilla JS, JSON data | stories-site/timeline/ |
Phase 3 |
| Period photo galleries | Lazy-load HTML (Task 35) | stories-site/photos/ |
Phase 3 |
| Thematic essays | Markdown → HTML | stories-site/essays/ |
Phase 4 |
| Short AI clips | MP4/WebM embed | stories-site/clips/ |
Phase 4 |
Google Drive Asset Pipeline
Source: Google Drive folder 1HyVg2PYyrUdq4bEh_qNh_kk3Ml7RaKr8
AI-generated illustrations created by Oleksii land in Google Drive first, then flow into the site:
Google Drive (Oleksii uploads)
│
▼
scripts/sync_drive_assets.py ← reads via Drive API (SA key from SSM)
│ downloads to stories-site/assets/illustrations/
│ enforces naming: ep{season}-{letter}-{short-title}.jpg
│
▼
CI: deploy-stories.yml ← syncs stories-site/ to S3
│
▼
CloudFront → stories.victor.ponomarenko.io/assets/illustrations/
The SA key (victor-bio-drive-reader@ponomarenko-vicktor-bio.iam.gserviceaccount.com) is stored in SSM /victor-bio/google/drive-sa-key and in GitHub secret GOOGLE_SA_KEY_JSON. The sync script runs as a CI step on demand (manual trigger or workflow_dispatch).
Phased Roadmap
Phase 1 — Auth & Foundation (Task 38)
Deliverables:
- CloudFront distribution for stories with Lambda@Edge auth (same functions as bio.victor)
- Route 53 A record + ACM cert for stories.victor.ponomarenko.io
- Separate SSM allowlist /story/prod/cognito/allowed-emails wired to stories-client
- deploy-stories.yml CI/CD workflow functional
- Feature flag SSM convention documented and first flag live (/story/prod/flags/illustrations-enabled)
- Canary deployment infrastructure scaffolded (even if weight is 0%)
Acceptance: stories.victor.ponomarenko.io serves content, requires login, uses separate allowlist.
Phase 2 — Illustrated Chapter Pages (Task 39)
Deliverables:
- Static HTML template for each of the 18 illustrated chapters (Oleksii's designs)
- Illustration ingestion from Google Drive via sync_drive_assets.py
- Chapter index page (/chapters/) listing all 18 episodes with thumbnails
- Lazy-load image loading for illustrations (no layout shift)
- Marp slides linked per chapter ("View full slide deck →")
Acceptance: all 18 chapter pages live, each with at least a placeholder illustration slot.
Phase 3 — Navigation & Homepage UX (Task 40)
Deliverables:
- Homepage (/) redesign: curated hero, featured episode, recent additions
- Site-wide navigation: Chapters | Gallery | Timeline | Essays
- Period photo galleries (Task 35 integration)
- Search / filter for chapters by part, period, style
- Mobile-responsive layout
Acceptance: first-time visitor can find any chapter within 2 taps.
Phase 4 — Interactive Features & RBAC (Tasks 41–42)
Deliverables: - Interactive timeline (1936–1975) — JS + JSON, no framework dependency - Short AI clip embeds (Task 20) - Thematic essay series (first essay live) - RBAC model: viewer / contributor / editor roles (Task 41) - viewers: read access (current allowlist model) - contributors: can submit content via GitHub PR - editors: can merge content, manage allowlist via admin UI
Security Requirements
- No S3 public access — OAC only, same as bio.victor
- No hardcoded credentials — OIDC roles for CI, SSM for secrets at runtime
- Allowlist enforced by Cognito pre-sign-up Lambda (existing) — same Lambda reads both SSM paths based on client ID
- Content not indexed by search engines:
X-Robots-Tag: noindex, nofollowheader via CloudFront response headers policy - All SSM parameters under
/story/prod/— separate from/victor-bio/namespace - No public API endpoints — site is static HTML only
Local Development
# Serve slides locally (port 8001 to avoid clash with bio.victor on 8000)
npx @marp-team/marp-cli slides/part-1/ --server --port 8001
# Sync Drive assets locally (requires Drive SA key in env)
GOOGLE_SA_KEY_SSM_PATH=/victor-bio/google/drive-sa-key \
uv run python scripts/sync_drive_assets.py --dest stories-site/assets/
# Preview full stories site
cd stories-site && python -m http.server 8001
Open Questions / Future Decisions
| # | Question | Decision needed by |
|---|---|---|
| 1 | Static HTML generator vs plain HTML files for chapter pages? (Jekyll, 11ty, or raw HTML) | Phase 2 start |
| 2 | Analytics: CloudFront logs only, or add Plausible/Umami for per-page stats? | Phase 3 |
| 3 | Comment/reaction system for collaborators? (GitHub Discussions, Disqus, custom) | Phase 4 |
| 4 | RBAC implementation: Cognito groups + Lambda@Edge, or separate DynamoDB table? | Task 41 |
| 5 | Stories site content in this repo or new repo? | Phase 2 (lean toward: same repo, stories-site/ dir) |