Skip to content

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, nofollow header 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)