Files
summercms-initial-research/.planning/phases/01-foundation/01-03-PLAN.md
Jakub Zych 3f1fc59d23 docs(01): create phase 1 foundation plans
Phase 01: Foundation
- 3 plans in 3 waves (sequential dependency)
- Plan 01: Mill build + ZIO HTTP server
- Plan 02: PostgreSQL + Quill + Flyway migrations
- Plan 03: Repository pattern + Pulumi/Besom deployment

Ready for execution
2026-02-04 17:21:15 +01:00

9.9 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-foundation 03 execute 3
01-02
src/repository/RepositoryError.scala
src/repository/UserRepository.scala
src/model/User.scala
infra/project.scala
infra/Main.scala
Dockerfile
true
truths artifacts key_links
Repository trait defines CRUD operations with typed ZIO errors
UserRepositoryLive implements queries using Quill context
Besom/Pulumi infrastructure code compiles and can run pulumi preview
Dockerfile builds fat JAR into runnable container
path provides exports
src/repository/RepositoryError.scala Typed error ADT for repository operations
RepositoryError
NotFound
Conflict
DatabaseError
path provides exports
src/repository/UserRepository.scala User repository trait and implementation
UserRepository
UserRepositoryLive
path provides exports
src/model/User.scala User domain model
User
path provides contains
infra/Main.scala Pulumi infrastructure definition Pulumi.run
path provides contains
Dockerfile Container build configuration FROM
from to via pattern
src/repository/UserRepository.scala src/db/QuillContext.scala Quill.Postgres dependency Quill.Postgres
from to via pattern
src/repository/UserRepository.scala src/model/User.scala CRUD operations on User User
from to via pattern
src/repository/UserRepository.scala src/repository/RepositoryError.scala IO[RepositoryError, _] return type RepositoryError
Implement the Repository pattern with typed errors and create Pulumi infrastructure configuration using Besom.

Purpose: Establish the data access pattern that all future models will follow, and prepare deployment infrastructure. Output: Working UserRepository with compile-time validated queries, Pulumi config for cloud deployment, Dockerfile for containerization.

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-01-SUMMARY.md @.planning/phases/01-foundation/01-02-SUMMARY.md Task 1: Create Repository pattern with typed errors src/model/User.scala src/repository/RepositoryError.scala src/repository/UserRepository.scala Implement the Repository pattern following ZIO best practices.
  1. src/model/User.scala:
package summercms.model

import java.time.Instant

case class User(
  id: Long,
  email: String,
  passwordHash: String,
  createdAt: Instant,
  updatedAt: Instant
)
  1. src/repository/RepositoryError.scala:
package summercms.repository

sealed trait RepositoryError

object RepositoryError:
  case class NotFound(entity: String, id: Long) extends RepositoryError
  case class NotFoundByField(entity: String, field: String, value: String) extends RepositoryError
  case class Conflict(entity: String, message: String) extends RepositoryError
  case class ValidationError(errors: List[String]) extends RepositoryError
  case class DatabaseError(message: String, cause: Option[Throwable] = None) extends RepositoryError
  1. src/repository/UserRepository.scala:
  • Trait UserRepository with methods:

    • def findById(id: Long): IO[RepositoryError, Option[User]]
    • def findByEmail(email: String): IO[RepositoryError, Option[User]]
    • def create(email: String, passwordHash: String): IO[RepositoryError, User]
    • def update(user: User): IO[RepositoryError, User]
    • def delete(id: Long): IO[RepositoryError, Unit]
  • Object UserRepository with:

    • val live: ZLayer[Quill.Postgres[SnakeCase], Nothing, UserRepository]
  • Class UserRepositoryLive(quill: Quill.Postgres[SnakeCase]) extends UserRepository:

    • import quill.*
    • inline def users = quote(querySchemaUser)
    • Implement all methods using Quill queries
    • Use refineOrDie to convert SQLException to RepositoryError.DatabaseError
    • Handle unique constraint violations as Conflict errors

Pattern for error handling:

def create(email: String, passwordHash: String): IO[RepositoryError, User] =
  val now = Instant.now()
  val user = User(0, email, passwordHash, now, now)
  run(users.insertValue(lift(user)).returningGenerated(_.id))
    .map(id => user.copy(id = id))
    .refineOrDie {
      case e: java.sql.SQLException if e.getSQLState == "23505" =>
        RepositoryError.Conflict("User", s"Email $email already exists")
      case e: java.sql.SQLException =>
        RepositoryError.DatabaseError(e.getMessage, Some(e))
    }

IMPORTANT: Do not use "io" as a variable name (Quill package conflict). mill compile succeeds, repository pattern established User model, RepositoryError ADT, and UserRepository trait+implementation exist and compile

Task 2: Create Pulumi infrastructure with Besom infra/project.scala infra/Main.scala Create Pulumi infrastructure using Besom (Scala Pulumi SDK).

IMPORTANT: Besom does NOT support Mill. Use Scala CLI for the infra/ directory.

  1. infra/project.scala (Scala CLI project config):
//> using scala 3.3.4
//> using dep org.virtuslab::besom-core:0.4.0
//> using dep org.virtuslab::besom-aws:6.56.0

Note: Use Besom 0.4.0 (latest stable on Maven Central, not 0.5.0 from research).

  1. infra/Main.scala:
import besom.*
import besom.api.aws

@main def main = Pulumi.run {
  // Configuration
  val config = besom.Config()
  val environment = config.get("environment").getOrElse("dev")

  // S3 bucket for assets (optional CDN origin)
  val assetsBucket = aws.s3.Bucket(
    s"summercms-assets-$environment",
    aws.s3.BucketArgs()
  )

  // RDS PostgreSQL instance
  val db = aws.rds.Instance(
    s"summercms-db-$environment",
    aws.rds.InstanceArgs(
      engine = "postgres",
      engineVersion = "16.4",
      instanceClass = "db.t3.micro",
      allocatedStorage = 20,
      dbName = "summercms",
      username = "summercms",
      password = config.requireSecret("dbPassword"),
      skipFinalSnapshot = environment == "dev",
      publiclyAccessible = environment == "dev"
    )
  )

  // ECS for container deployment (placeholder for now)
  // Full ECS setup deferred to when we need actual deployment

  Stack.exports(
    assetsBucketName = assetsBucket.bucket,
    dbEndpoint = db.endpoint,
    dbPort = db.port
  )
}

This provides:

  • Multi-environment support via Pulumi config
  • S3 bucket for future asset storage
  • RDS PostgreSQL for database
  • Secrets management for DB password
  • Outputs for connecting the application

Note: Full ECS/container deployment is deferred. This establishes the pattern. Run from infra/ directory:

  • scala-cli compile . compiles
  • pulumi preview shows resources (requires Pulumi CLI and AWS credentials) Besom infrastructure code compiles, defines RDS and S3 resources
Task 3: Create Dockerfile for containerization Dockerfile Create multi-stage Dockerfile for building and running SummerCMS.

Dockerfile:

# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder

# Install Mill
RUN curl -L https://github.com/com-lihaoyi/mill/releases/download/0.12.3/0.12.3 > /usr/local/bin/mill && \
    chmod +x /usr/local/bin/mill

WORKDIR /app
COPY . .

# Build fat JAR
RUN mill assembly

# Stage 2: Runtime
FROM eclipse-temurin:21-jre

WORKDIR /app

# Copy the fat JAR from builder
COPY --from=builder /app/out/assembly.dest/out.jar /app/summercms.jar

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# Install curl for health check
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "/app/summercms.jar"]

Features:

  • Multi-stage build (builder has JDK+Mill, runtime has only JRE)
  • Uses Eclipse Temurin JDK 21 (LTS)
  • Health check using /health endpoint
  • Minimal runtime image size
  • Environment variables configure the app (via application.conf ${?VAR} substitution)

Usage:

docker build -t summercms .
docker run -p 8080:8080 \
  -e DB_HOST=host.docker.internal \
  -e DB_PASSWORD=secret \
  summercms
`docker build -t summercms .` succeeds `docker run -p 8080:8080 summercms` starts (will fail at DB connect without DB, that's expected) Dockerfile exists with multi-stage build, health check, and env var configuration 1. `mill compile` succeeds (repository code compiles) 2. Repository pattern test (manual in mill console or separate test): - UserRepository.findById returns IO[RepositoryError, Option[User]] - Type errors when using wrong types 3. Infrastructure: - `cd infra && scala-cli compile .` succeeds - `cd infra && pulumi preview` shows planned resources (requires setup) 4. Docker: - `docker build -t summercms .` builds successfully - Image contains /app/summercms.jar

<success_criteria>

  • User model and RepositoryError ADT defined
  • UserRepository trait with CRUD methods, typed errors
  • UserRepositoryLive implements queries with Quill
  • Besom infrastructure defines RDS and S3 for AWS
  • Dockerfile builds fat JAR in multi-stage build
  • All code compiles without errors </success_criteria>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`