Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,41 @@ jobs:
exit 1
fi

# Broken-link audit: relative markdown links to local source files (.cs/.csproj/.props/
# .yml/.svg/.excalidraw/.md) that don't resolve. Catches drift from file renames and
# the kind of fallout the simplicity refactor produced — CLAUDE.md citations pointing at
# deleted PaymentRepository.cs, demo docs pointing at the pre-VSA-collapse 4-project
# layout. Same shape as the static-mutable-collections check above: grep + fail. Skips
# external URLs (https?://) and anchors-only (#section); resolves paths relative to
# the markdown file's directory. Uses process substitution everywhere so the failure
# flag survives the loop (a piped while runs in a subshell and loses its updates).
- name: Broken-link audit — markdown citations to local files
run: |
fail=0
while IFS= read -r mdfile; do
dir=$(dirname "$mdfile")
while IFS= read -r link; do
case "$link" in http*|//*) continue ;; esac
candidate="${link%%#*}"
candidate="${candidate%%\?*}"
if [ "$dir" = "." ]; then target="$candidate"; else target="$dir/$candidate"; fi
if [ ! -e "$target" ] && [ ! -e "$candidate" ]; then
echo "::error file=$mdfile::broken link → $link"
Comment on lines +129 to +131
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Repo-root fallback masks genuinely broken relative links.

The second existence check ([ ! -e "$candidate" ]) can let an invalid markdown-relative link pass if a same-named file exists at repo root. This weakens the guard’s core purpose.

Suggested patch
-              if [ ! -e "$target" ] && [ ! -e "$candidate" ]; then
+              if [ ! -e "$target" ]; then
                 echo "::error file=$mdfile::broken link → $link"
                 fail=1
               fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 129 - 131, The existence check
currently falls back to checking "$candidate" (repo-root) which masks broken
relative links; update the conditional so it only verifies the resolved path for
the markdown file (i.e., test only "$target" which is built from
"$dir/$candidate" or "." handling) instead of also checking "$candidate", or
otherwise resolve/normalize "$candidate" against "$dir" before testing; adjust
the if that uses variables target, candidate, mdfile, and link so a same-named
file at repo root no longer makes a broken relative link pass.

fail=1
fi
done < <(grep -oE '\[[^]]+\]\(([^)#]+\.(cs|csproj|props|sh|yml|yaml|svg|excalidraw|cls|md))[^)#]*\)' "$mdfile" \
| sed -E 's/.*\(([^)]+)\)/\1/')
done < <(find . -type f -name '*.md' \
-not -path './bin/*' -not -path './obj/*' \
-not -path '*/node_modules/*' -not -path '*/.git/*' \
-not -path './.claude/audits/INDEX.md')
exit "$fail"
Comment on lines +121 to +140
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add strict bash mode to this audit step.

This run block should start with set -euo pipefail; otherwise command failures can be silently ignored and the audit may report false success.

Suggested patch
       - name: Broken-link audit — markdown citations to local files
         run: |
+          set -euo pipefail
           fail=0
           while IFS= read -r mdfile; do

As per coding guidelines: “DO flag: ... missing set -euo pipefail in bash run blocks.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: |
fail=0
while IFS= read -r mdfile; do
dir=$(dirname "$mdfile")
while IFS= read -r link; do
case "$link" in http*|//*) continue ;; esac
candidate="${link%%#*}"
candidate="${candidate%%\?*}"
if [ "$dir" = "." ]; then target="$candidate"; else target="$dir/$candidate"; fi
if [ ! -e "$target" ] && [ ! -e "$candidate" ]; then
echo "::error file=$mdfile::broken link → $link"
fail=1
fi
done < <(grep -oE '\[[^]]+\]\(([^)#]+\.(cs|csproj|props|sh|yml|yaml|svg|excalidraw|cls|md))[^)#]*\)' "$mdfile" \
| sed -E 's/.*\(([^)]+)\)/\1/')
done < <(find . -type f -name '*.md' \
-not -path './bin/*' -not -path './obj/*' \
-not -path '*/node_modules/*' -not -path '*/.git/*')
exit "$fail"
run: |
set -euo pipefail
fail=0
while IFS= read -r mdfile; do
dir=$(dirname "$mdfile")
while IFS= read -r link; do
case "$link" in http*|//*) continue ;; esac
candidate="${link%%#*}"
candidate="${candidate%%\?*}"
if [ "$dir" = "." ]; then target="$candidate"; else target="$dir/$candidate"; fi
if [ ! -e "$target" ] && [ ! -e "$candidate" ]; then
echo "::error file=$mdfile::broken link → $link"
fail=1
fi
done < <(grep -oE '\[[^]]+\]\(([^)#]+\.(cs|csproj|props|sh|yml|yaml|svg|excalidraw|cls|md))[^)#]*\)' "$mdfile" \
| sed -E 's/.*\(([^)]+)\)/\1/')
done < <(find . -type f -name '*.md' \
-not -path './bin/*' -not -path './obj/*' \
-not -path '*/node_modules/*' -not -path '*/.git/*')
exit "$fail"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 121 - 139, Add strict bash mode to the
CI Markdown-audit run block by inserting "set -euo pipefail" at the top of the
block (before "fail=0") so failures in the pipeline or unset variables cause the
job to fail; this change applies to the run block containing "fail=0" and the
loop starting with "while IFS= read -r mdfile; do" to ensure the audit cannot
silently succeed on command errors.

# `.claude/audits/INDEX.md` is intentionally excluded — it links to per-article audit
# files under `.claude/audits/*.md` that are gitignored for copyright reasons (they
# contain verbatim quoted prose from external articles). See `.claude/commands/article-audit.md`
# step 5 "Copyright note" — the contract is "INDEX ships, per-article files don't."
# On a contributor's machine the links resolve; on the CI runner they don't, by design.
Comment on lines +141 to +145
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in file path pattern.

Line 142 contains a duplicated path segment. The pattern .claude/audits/*.claude/audits/*.md should be .claude/audits/*.md.

📝 Suggested fix
           # `.claude/audits/INDEX.md` is intentionally excluded — it links to per-article audit
-          # files under `.claude/audits/*.claude/audits/*.md` that are gitignored for copyright reasons (they
+          # files under `.claude/audits/*.md` that are gitignored for copyright reasons (they
           # contain verbatim quoted prose from external articles). See `.claude/commands/article-audit.md`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# `.claude/audits/INDEX.md` is intentionally excluded — it links to per-article audit
# files under `.claude/audits/*.md` that are gitignored for copyright reasons (they
# contain verbatim quoted prose from external articles). See `.claude/commands/article-audit.md`
# step 5 "Copyright note" — the contract is "INDEX ships, per-article files don't."
# On a contributor's machine the links resolve; on the CI runner they don't, by design.
# `.claude/audits/INDEX.md` is intentionally excluded — it links to per-article audit
# files under `.claude/audits/*.md` that are gitignored for copyright reasons (they
# contain verbatim quoted prose from external articles). See `.claude/commands/article-audit.md`
# step 5 "Copyright note" — the contract is "INDEX ships, per-article files don't."
# On a contributor's machine the links resolve; on the CI runner they don't, by design.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 141 - 145, Fix the duplicated path
segment in the CI workflow exclude pattern by replacing the incorrect glob
`.claude/audits/*.claude/audits/*.md` with the correct `.claude/audits/*.md` in
the `.github/workflows/ci.yml` file so the exclude rule targets files under the
`.claude/audits` directory correctly.


# Testcontainers-based integration tests, in their own job: they need Docker (the
# ubuntu-latest runner ships it at the standard /var/run/docker.sock, so Testcontainers
# auto-detects — no DOCKER_HOST override, unlike macOS Docker Desktop locally). Kept
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ public async Task ExecuteInTransactionAsync(Func<CancellationToken, Task> work,
await tx.CommitAsync(ct);
}
```
Reference: [PaymentRepository.ExecuteInTransactionAsync](PaymentService/Infrastructure/PaymentRepository.cs) (fixed in the commit captured by docs/STATUS.md). When adding a non-handler code path that publishes events, **either** wrap it in this pattern **or** factor the publish back into a Wolverine handler triggered by an internal scheduled message.
Reference: [PaymentRecoveryJob](PaymentService/Infrastructure/PaymentRecoveryJob.cs) — the canonical inline implementation of this wrapper (the previous `IPaymentRepository.ExecuteInTransactionAsync` wrapper was deleted in the simplicity refactor; the pattern itself is unchanged, just inlined). When adding a non-handler code path that publishes events, **either** wrap it in this pattern **or** factor the publish back into a Wolverine handler triggered by an internal scheduled message.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the mandatory CLAUDE.md audit note.

Please append the required note in this CLAUDE.md change context: "Run /check-rules locally to audit paraphrases against this diff."

As per coding guidelines: “When this changes… Flag the CLAUDE.md change with a note: ‘Run /check-rules locally to audit paraphrases against this diff.’”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 341, Append the mandated audit note sentence "Run
/check-rules locally to audit paraphrases against this diff." to the CLAUDE.md
change context referenced by the PaymentRecoveryJob example; ensure the note
appears alongside the existing guidance about wrapping non-handler publish paths
(or factoring to a Wolverine handler) and mention of the removed
IPaymentRepository.ExecuteInTransactionAsync wrapper so reviewers see the
required local check instruction in the same paragraph/context.


### Event Replay

Expand Down
26 changes: 15 additions & 11 deletions Dockerfile.catalog
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Multi-stage build for CatalogService.Api targeting single-service demo deploys (App Runner,
# Container Apps, Lightsail, etc.). Build context is the repo root because the API project
# transitively references ServiceDefaults, Contracts, Application, Infrastructure, Domain.
# Multi-stage build for CatalogService targeting single-service demo deploys (Fly.io,
# App Runner, Container Apps, Lightsail, etc.). Build context is the repo root because
# CatalogService transitively references NextAurora.ServiceDefaults and NextAurora.Contracts.
#
# Build: docker build -f Dockerfile.catalog -t catalog-api .
# Run: docker run --rm -p 8080:8080 \
Expand All @@ -9,6 +9,11 @@
# -e DemoMode=true \
# -e ConnectionStrings__catalog-db="Host=...;Database=...;Username=...;Password=..." \
# catalog-api
#
# Single-project layout (post the VSA-collapse refactor — see CLAUDE.md "Project Structure"):
# CatalogService is one Web SDK project under CatalogService/, not the four-project Clean
# layout that existed up to PR #31. If you're rolling back to a pre-#31 commit, this Dockerfile
# won't build — that's intentional, the pre-#31 Dockerfile is preserved in git history.

# ─── Build stage ──────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
Expand All @@ -19,23 +24,22 @@ WORKDIR /src
# their stricter defaults — CA2007/CA1062/CA1724 etc. become errors under TreatWarningsAsErrors).
COPY .editorconfig Directory.Build.props Directory.Packages.props BannedSymbols.txt NextAurora.slnx ./

# Copy only the project files needed for the Catalog dependency graph.
COPY CatalogService/CatalogService.Api/CatalogService.Api.csproj CatalogService/CatalogService.Api/
COPY CatalogService/CatalogService.Application/CatalogService.Application.csproj CatalogService/CatalogService.Application/
COPY CatalogService/CatalogService.Domain/CatalogService.Domain.csproj CatalogService/CatalogService.Domain/
COPY CatalogService/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj CatalogService/CatalogService.Infrastructure/
# Copy only the project files needed for the Catalog dependency graph. csproj-then-source is
# the standard Docker .NET layer-caching pattern: the restore layer only invalidates when a
# csproj actually changes, not on every source edit.
COPY CatalogService/CatalogService.csproj CatalogService/
COPY NextAurora.ServiceDefaults/NextAurora.ServiceDefaults.csproj NextAurora.ServiceDefaults/
COPY NextAurora.Contracts/NextAurora.Contracts.csproj NextAurora.Contracts/

RUN dotnet restore CatalogService/CatalogService.Api/CatalogService.Api.csproj
RUN dotnet restore CatalogService/CatalogService.csproj

# Copy the actual source after restore — keeps the dependency-graph layer cached unless a
# .csproj actually changes.
COPY CatalogService/ CatalogService/
COPY NextAurora.ServiceDefaults/ NextAurora.ServiceDefaults/
COPY NextAurora.Contracts/ NextAurora.Contracts/

RUN dotnet publish CatalogService/CatalogService.Api/CatalogService.Api.csproj \
RUN dotnet publish CatalogService/CatalogService.csproj \
--configuration Release \
--output /app/publish \
--no-restore \
Expand All @@ -56,4 +60,4 @@ EXPOSE 8080
USER app

COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "CatalogService.Api.dll"]
ENTRYPOINT ["dotnet", "CatalogService.dll"]
27 changes: 11 additions & 16 deletions docs/demo-deployment-story.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Deployment Story — Getting CatalogService Live on Fly.io

A step-by-step narrative of what we actually did to deploy [CatalogService.Api](../CatalogService/CatalogService.Api/) to a public URL, including the dead ends and why we ended up where we did. Useful for walking someone through the deployment story or refreshing your own memory.
A step-by-step narrative of what we actually did to deploy [CatalogService](../CatalogService/) to a public URL, including the dead ends and why we ended up where we did. Useful for walking someone through the deployment story or refreshing your own memory.

For the reusable checklist (do this from scratch), see [demo-deployment.md](demo-deployment.md). This doc is the *story*; that doc is the *recipe*.

---

## Goal

Get a working public URL serving `CatalogService.Api` with the Scalar API documentation reachable, on as little budget and complexity as possible — without breaking any of the existing local development paths (Aspire, integration tests, future production deploy).
Get a working public URL serving `CatalogService` with the Scalar API documentation reachable, on as little budget and complexity as possible — without breaking any of the existing local development paths (Aspire, integration tests, future production deploy).

## What "done" looks like

Expand Down Expand Up @@ -39,9 +39,9 @@ Get a working public URL serving `CatalogService.Api` with the Scalar API docume

## Step 1 — Make the code deploy-aware (`DemoMode` flag)

**Problem**: `CatalogService.Api` was wired for two environments — local development (where Scalar/OpenAPI are exposed) and a hypothetical production (where they're hidden because OpenAPI specs are reconnaissance gold). For the demo we needed a *third* mode: Production-environment behavior PLUS Scalar visibility, because the whole point is showing the API documentation.
**Problem**: `CatalogService` was wired for two environments — local development (where Scalar/OpenAPI are exposed) and a hypothetical production (where they're hidden because OpenAPI specs are reconnaissance gold). For the demo we needed a *third* mode: Production-environment behavior PLUS Scalar visibility, because the whole point is showing the API documentation.

**Solution**: a `DemoMode` configuration flag in [Program.cs](../CatalogService/CatalogService.Api/Program.cs). When set, it:
**Solution**: a `DemoMode` configuration flag in [Program.cs](../CatalogService/Program.cs). When set, it:
1. Exposes `/openapi/v1.json`, `/openapi/v1.yaml`, `/scalar/v1` even outside Development
2. Skips `UseHttpsRedirection()` (PaaS hosts terminate TLS at the edge — would cause redirect loops)
3. Runs EF Core migrations on startup (so we don't need a separate "deploy migrations" step)
Expand All @@ -50,7 +50,7 @@ Get a working public URL serving `CatalogService.Api` with the Scalar API docume

## Step 2 — Make Redis optional

`CatalogService.Infrastructure` registers Redis via HybridCache's L2 tier. For a single-replica demo we don't want to pay for managed Redis. The registration is now conditional: if no `cache` connection string is configured, Redis isn't registered, and HybridCache gracefully degrades to L1-only (in-process MemoryCache). When run via Aspire locally, Redis IS registered because `WithReference(cache)` provides the connection string — so local dev is unchanged.
`CatalogService.Infrastructure` (the `Infrastructure/` folder inside the single CatalogService project) registers Redis via HybridCache's L2 tier. For a single-replica demo we don't want to pay for managed Redis. The registration is now conditional: if no `cache` connection string is configured, Redis isn't registered, and HybridCache gracefully degrades to L1-only (in-process MemoryCache). When run via Aspire locally, Redis IS registered because `WithReference(cache)` provides the connection string — so local dev is unchanged.

## Step 3 — Containerize

Expand Down Expand Up @@ -102,7 +102,7 @@ Postgres provisioning prints the connection details once — **password is unrec

**Problem**: Fly's secret names only allow `[A-Z0-9_]` — hyphens are rejected. But our app reads `GetConnectionString("catalog-db")` (kebab-case, set by Aspire's `WithReference()` convention). The corresponding env var name would be `ConnectionStrings__catalog-db`, which Fly bounces.

**Solution**: a tiny adapter in [Program.cs](../CatalogService/CatalogService.Api/Program.cs) that, only when `DemoMode=true`, reads from a Fly-compatible secret name (`CATALOG_DB_CONNECTION_STRING`) and copies it into the `ConnectionStrings:catalog-db` slot the Infrastructure layer reads from. 5 lines, fully gated behind the demo flag, doesn't touch Aspire wiring.
**Solution**: a tiny adapter in [Program.cs](../CatalogService/Program.cs) that, only when `DemoMode=true`, reads from a Fly-compatible secret name (`CATALOG_DB_CONNECTION_STRING`) and copies it into the `ConnectionStrings:catalog-db` slot the Infrastructure layer reads from. 5 lines, fully gated behind the demo flag, doesn't touch Aspire wiring.

Then set the secret:

Expand Down Expand Up @@ -138,7 +138,7 @@ This is the one decision in the demo deploy that deliberately *violates* a produ

### What actually happens

[Program.cs](../CatalogService/CatalogService.Api/Program.cs) ends its startup with:
[Program.cs](../CatalogService/Program.cs) ends its startup with:

```csharp
if (app.Environment.IsDevelopment() || isDemoMode)
Expand Down Expand Up @@ -194,9 +194,7 @@ The `xmin` system column is Postgres-specific — it's the transaction ID of the
If we change a domain entity later (e.g. add a `Sku` field to `Product`):

```bash
dotnet ef migrations add AddProductSku \
--project CatalogService/CatalogService.Infrastructure \
--startup-project CatalogService/CatalogService.Api
dotnet ef migrations add AddProductSku --project CatalogService
```

This generates a new `.cs` file in `Migrations/`. Commit it. Next `fly deploy --remote-only` ships the new code + new migration, the Machine reboots, `Migrate()` notices `AddProductSku` is unapplied, runs the `ALTER TABLE` it contains, and the new boot is serving with the new schema. Zero downtime if the change is backward-compatible (additive columns, new indexes, new tables). Forward-incompatible changes (drop column, rename, NOT NULL on existing column) need the multi-step plan described in [ef-core.md "The immutable-once-applied rule"](ef-core.md#67-the-immutable-once-applied-rule).
Expand All @@ -207,17 +205,14 @@ After the first deploy worked, the catalog was empty (`GET /api/v1/products` ret

### Adding the seed

In [CatalogDbContext.cs](../CatalogService/CatalogService.Infrastructure/Data/CatalogDbContext.cs), `OnModelCreating` calls a private `SeedDemoData` method that uses `modelBuilder.Entity<T>().HasData(...)` to declaratively register 3 categories and 7 products. Fixed GUIDs and a fixed `CreatedAt` (not `Guid.NewGuid()` / `DateTime.UtcNow`) so the generated migration is **deterministic** — re-running the model snapshot wouldn't emit a diff.
In [CatalogDbContext.cs](../CatalogService/Infrastructure/Data/CatalogDbContext.cs), `OnModelCreating` calls a private `SeedDemoData` method that uses `modelBuilder.Entity<T>().HasData(...)` to declaratively register 3 categories and 7 products. Fixed GUIDs and a fixed `CreatedAt` (not `Guid.NewGuid()` / `DateTime.UtcNow`) so the generated migration is **deterministic** — re-running the model snapshot wouldn't emit a diff.

`HasData` writes via reflection, which **bypasses** the entity's factory method (`Product.Create`) and private setters. That's the right trade for curated design-time data — validation is unnecessary because we control the values. We still set `IsAvailable` explicitly to match the `StockQuantity > 0` invariant the factory would have enforced.

Then generate the migration:

```bash
dotnet ef migrations add SeedDemoCatalog \
--project CatalogService/CatalogService.Infrastructure \
--startup-project CatalogService/CatalogService.Api \
--context CatalogDbContext
dotnet ef migrations add SeedDemoCatalog --project CatalogService --context CatalogDbContext
```

EF Core produced two files:
Expand Down Expand Up @@ -323,7 +318,7 @@ In rough order:
2. **Local Docker daemon was corrupted** from an earlier disk-full event. → `--remote-only` builds on Fly's builder, sidestepping local Docker entirely.
3. **Fly removed dashboard-level spending caps**; only soft alerts remain. → Bought $25 prepaid credits and didn't save a card. When credits hit $0, Fly suspends instead of charging. Effective hard cap.
4. **Fly's `fly postgres create` warns it's "unmanaged"** and pushes Managed Postgres ($15+/mo). → Legacy unmanaged is fine for throwaway demo data; ignored the nudge.
5. **Fly secret names reject hyphens** (`[A-Z0-9_]` only). → Added a `DemoMode`-only bridge in [Program.cs](../CatalogService/CatalogService.Api/Program.cs) that copies `CATALOG_DB_CONNECTION_STRING` into `ConnectionStrings:catalog-db`. Aspire wiring untouched.
5. **Fly secret names reject hyphens** (`[A-Z0-9_]` only). → Added a `DemoMode`-only bridge in [Program.cs](../CatalogService/Program.cs) that copies `CATALOG_DB_CONNECTION_STRING` into `ConnectionStrings:catalog-db`. Aspire wiring untouched.
6. **Docker build failed: analyzer errors (CA1062/CA2007/CA1724/MA0004) under `TreatWarningsAsErrors=true`.** → The `.editorconfig` at the repo root suppresses these; wasn't being copied into the build context. Added to the COPY line in [Dockerfile.catalog](../Dockerfile.catalog).
7. **First deploy crashed: `Exception while performing SSL handshake / Received an unexpected EOF`** on the EF Core migration's first Postgres connection. → Fly's legacy unmanaged Postgres on `.flycast` doesn't speak SSL. Npgsql's default `SSL Mode=Prefer` crashes hard instead of falling back to plain. Fix: append `SSL Mode=Disable` to the connection string. Flycast is already a private encrypted network, so disabling Postgres-layer SSL is safe inside that perimeter.
8. **Health-check grace period was too short** for first boot (20s default vs ~30-60s for migration + Postgres connect). → Bumped to 120s in fly.toml. Subsequent boots are fast because `Migrate()` finds the migration already applied and returns in ms.
Expand Down
Loading
Loading