7.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repo layout
.
├── cms/ Go service: video ingestion/CMS (implemented)
├── discovery/ Go service: empty placeholder, no code yet
├── infrastructure/ Pulumi (Go) program provisioning all AWS resources
└── .gitea/workflows/ Gitea Actions CI/CD pipelines
discovery is provisioned in infra (its own ECR repo, ECS service, ALB,
Postgres database/role) but has no application code yet — don't assume it's
deployable.
Commands
cms (Go 1.25, module thamanyah/cms/v2)
cd cms
go run . # serves on :8081
go build ./...
go vet ./...
There are no test files in this repo (cms, discovery, or infrastructure) — don't assume a test suite exists.
Views are written as .templ files (github.com/a-h/templ) and compiled to
*_templ.go. If you edit a .templ file, regenerate its Go code with the
templ generate CLI before building (not installed in this environment by
default — install via go install github.com/a-h/templ/cmd/templ@v0.3.1020
to match go.mod, or check for an existing binary first).
infrastructure (Go, Pulumi, module thamanyah)
cd infrastructure
pulumi preview # plan changes against stack "main"
pulumi up # apply — this touches real AWS resources, confirm with the user first
pulumi stack output # e.g. ecsClusterArn, cmsServiceArn
Deploys run in CI (.gitea/workflows/infrastructure-deploy.yml) on push to
main touching infrastructure/**. Treat local pulumi up as something to
confirm with the user, not a routine dev command — it mutates shared cloud
state and Pulumi state isn't safe to update concurrently with CI.
Architecture
cms service
Plain net/http (Go 1.22+ pattern-based ServeMux), no framework. Entry
point cms/main.go wires up AWS SDK v2 clients (S3, MediaConvert) from env
vars and assigns them to package-level interface vars in internal/services
(services.S3Client, services.MediaConvertClient) — handlers call through
these interfaces, and S3Concrete/MediaConvertConcrete are the only
implementations, which is what makes the handlers testable even though no
tests exist yet. On boot, S3Concrete.AssertSuccessfulConnection proactively
exercises head/put/get/presign against the bucket and panics on failure,
rather than letting the service come up in a broken state.
Routes (cms/main.go): GET /, GET /health, GET /videos/new,
POST /videos/presign, POST /videos, static files under /static/.
Views live in cms/internal/views (templ components) with a shared
layouts.Layout wrapper; types.go holds view-model structs like
VideoMetadata used by the upload-success page.
Video upload → transcode pipeline
- Browser calls
POST /videos/presign→ cms returns a presigned S3PUTURL forraw-uploads-bucket, keyvideos/<random-hex>.<ext>. - Browser
PUTs the file directly to S3 (requires the bucket's CORS rule, set up ininfrastructure/main.go). - Browser calls
POST /videoswith the metadata + key → cms callsMediaConvertClient.QueueEncodingJob(key), submitting a MediaConvert jobs3://raw-uploads-bucket/<key>→s3://encoded-bucket/<key>(H.264/AAC → MP4, QVBR rate control — QVBR requiresMaxBitrateto be set explicitly). - Finished output lands in
encoded-bucket, served via CloudFront.
Config wiring: cms reads S3_BUCKET, MEDIACONVERT_INPUT_BUCKET,
MEDIACONVERT_OUTPUT_BUCKET, MEDIACONVERT_ROLE_ARN, AWS_REGION from env
vars injected by the ECS task definition (extraEnv in
deployFargateService, infrastructure/main.go).
infrastructure (infrastructure/main.go, single Pulumi Go program, region us-east-1, stack main)
- Postgres: one shared RDS instance (
db.t3.micro, single-AZ, no backups — intentionally minimal). Each app (cms,discovery) gets its own login role and same-named database via thepostgresqlprovider (newServiceDatabase), so services never share DB credentials. - ECS Fargate: one cluster (
app-cluster), one ALB per service (each gets its own DNS name rather than sharing a load balancer on different ports).deployFargateService(...)is the shared helper building a service's ECR repo, CloudWatch log group, task definition, ECS service, and ALB.taskRoleis optional (nil = no AWS identity beyond the shared execution role);extraEnvappends container env vars beyond the DB_* set. - S3 + CloudFront:
encoded-bucketholds finished transcoded output, served publicly via CloudFront using Origin Access Control (OAC) — the bucket itself blocks all public access; only CloudFront's OAC principal can read it. - S3 raw uploads:
raw-uploads-bucketis a separate, private bucket for pre-transcode uploads — deliberately kept apart fromencoded-bucketso raw source video is never reachable through the public CDN. CORS is scoped toPUTonly, from thecmsALB's own origin (browser uploads directly via presigned URL). - IAM roles — four distinct roles/users, each scoped narrowly, don't
conflate them:
ecs-task-execution-role— shared by both services' ECS agent (image pull, log write, Secrets Manager read for DB password). Not usable by application code inside the container.cms-task-role— thecmscontainer's own AWS identity: S3ListBucket/PutObject/GetObjectonraw-uploads-bucketonly,mediaconvert:CreateJob, andiam:PassRolescoped to the MediaConvert service role (iam:PassedToServicecondition).discoveryhas no task role — it doesn't touch S3 or MediaConvert.mediaconvert-service-role— trusted bymediaconvert.amazonaws.com, not by ECS; the role MediaConvert itself assumes (passed asCreateJobInput.Role) to readraw-uploads-bucketand writeencoded-bucket. Distinct fromcms-task-roleby design: one is "cms calling AWS", the other is "AWS calling AWS on cms's behalf".gitea-ci-user— an IAM user (static access keys, not OIDC — the Gitea Actions runner doesn't support instance-profile auth) scoped to just ECR push (cms/discoveryrepos) andecs:UpdateService/ecs:DescribeServiceson the two ECS services. Never broaden this toecr:*/ecs:*.
CI/CD (.gitea/workflows/)
cms-deploy.yml: push tomaintouchingcms/**. Builds/pushes the Docker image to ECR usinggitea-ci-user, installs the AWS CLI (not preinstalled on the runner image — via AWS's official install script, not a third-party action), thenaws ecs update-service --force-new-deployment. Cluster/service ARNs come frompulumi stack outputand are set as repo variables (not secrets — ARNs aren't sensitive).infrastructure-deploy.yml: push tomaintouchinginfrastructure/**. Runspulumi upusing a separate, broader AWS credential (PULUMI_AWS_ACCESS_KEY_ID/SECRET) thangitea-ci-user, since provisioning IAM/RDS/ECS/CloudFront needs wider permissions than pushing images and forcing deployments. Guarded with a concurrency group since Pulumi state isn't safe to update concurrently.- The runner's
ubuntu-latestlabel maps to a docker image configured on the runner host (outside this repo) — currently minimal, lacking the AWS CLI, hence the manual install step incms-deploy.yml.
Known gaps
cms/internal/services/mediaconvert.gonever callsDescribeEndpointsand configures no custom MediaConvert endpoint — relies on the SDK's default regional endpoint.discoveryhas infra provisioned (ECR repo, ECS service, ALB, Postgres DB/role) but no application code.- No automated tests exist for
cms,discovery, orinfrastructure.