diff --git a/.dockerignore b/.dockerignore index 957ef8c..d3aaeae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ node_modules **/*.log tmp dist +.docker/buildx-cache diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7c4af1e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ +# AGENTS.md + +This file gives future coding agents a fast path map for this repository. + +## Repository map + +- API entrypoint: `cmd/api/main.go` +- HTTP routes/handlers: `internal/http/handlers.go` +- Core domain logic: `internal/app/service.go` +- In-memory persistence: `internal/store/` +- Auth utilities: `internal/auth/` +- Frontend static app: `web/` +- TypeScript API client: `libs/geo-api-client/` +- CI workflow: `.gitea/workflows/ci.yml` +- Architecture/planning docs: `docs/` + +## Most common commands + +From repo root: + +```bash +go test ./... +go run ./cmd/api +docker compose up --build -d +docker compose down +``` + +TypeScript client: + +```bash +cd libs/geo-api-client +bun install +bun test +bun run build +``` + +## Path conventions + +- Use repository-relative paths in docs and comments (never absolute machine paths). +- Keep API route changes in `internal/http/handlers.go`. +- Keep business rule changes in `internal/app/service.go`. +- Keep frontend integration docs under `docs/`. + +## Editing guidance for agents + +- Prefer minimal changes and avoid unrelated refactors. +- Add tests when behavior changes. +- Verify Go tests after backend changes. +- Verify Bun tests after TS client changes. +- If CI fails due runner/network infrastructure, keep logs explicit in workflow output. diff --git a/Dockerfile b/Dockerfile index a0ff140..a35bd05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.7 -FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder +FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS base ARG TARGETOS ARG TARGETARCH @@ -15,12 +15,19 @@ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download COPY . . + +FROM base AS builder RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ go build -p "$(nproc)" -trimpath -ldflags="-s -w" -o /out/api ./cmd/api -FROM gcr.io/distroless/static-debian12:nonroot +FROM base AS dev +ENV ADDR=:8080 +EXPOSE 8080 +CMD ["go", "run", "./cmd/api"] + +FROM gcr.io/distroless/static-debian12:nonroot AS runtime WORKDIR /app COPY --from=builder /out/api /app/api diff --git a/README.md b/README.md index 3f804fa..daab701 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Optional environment variables: Build and run the backend service: ```bash -docker compose up --build -d +COMPOSE_BAKE=true docker compose up --build -d ``` Stop the service: @@ -44,20 +44,26 @@ docker compose down For local development with auto-rebuild on file changes: ```bash -docker compose up --watch +COMPOSE_BAKE=true docker compose --profile dev up --watch ``` +Notes: + +- `api` service uses the production `runtime` image target. +- `api-dev` profile uses the `dev` image target and Docker Compose watch. +- Build cache is persisted at `.docker/buildx-cache` via `cache_from`/`cache_to`. + ## Frontend -Open `web/index.html` through a static server (recommended) or browser file URL. +Frontend is served by the Go backend at runtime. Example: ```bash -python -m http.server 4173 +go run ./cmd/api ``` -Then visit `http://localhost:4173/web/`. +Then visit `http://localhost:8080/web/`. ## API client library @@ -70,6 +76,10 @@ bun test bun run build ``` +Frontend TypeScript integration guide: + +- `docs/typescript-frontend-integration.md` + ## CI Workflow: `.gitea/workflows/ci.yml` diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..42928fc --- /dev/null +++ b/SKILL.md @@ -0,0 +1,50 @@ +--- +name: backend-fast-path +description: Quick orientation and execution paths for agents working in this repository. +--- + +# Skill: Backend Fast Path + +Use this skill when implementing or debugging backend, frontend integration, TypeScript client, or CI behavior in this repository. + +## Key paths + +- API bootstrap: `cmd/api/main.go` +- HTTP layer: `internal/http/handlers.go` +- Service logic: `internal/app/service.go` +- Tests (Go): `internal/http/api_test.go` +- Frontend app (served by Go): `web/index.html`, `web/app.js`, `web/api.js` +- TS client source: `libs/geo-api-client/src/` +- TS client tests: `libs/geo-api-client/test/` +- CI pipeline: `.gitea/workflows/ci.yml` +- Dev docs: `README.md`, `docs/typescript-frontend-integration.md`, `docs/geo-auth-backend-plan.md` + +## Task routing rules + +1. API behavior changes + - Update `internal/app/service.go` first. + - Then wire request/response changes in `internal/http/handlers.go`. + - Add/update tests in `internal/http/api_test.go`. + +2. Frontend integration changes + - Update TS client first in `libs/geo-api-client/src/`. + - Then update `web/` integration usage if needed. + - Document usage changes in `docs/typescript-frontend-integration.md`. + +3. CI failures + - Inspect `.gitea/workflows/ci.yml`. + - Keep logs actionable. + - Preserve explicit warnings when infrastructure prevents source checkout/tests. + +## Verification checklist + +```bash +go test ./... +cd libs/geo-api-client && bun test && bun run build +``` + +If Docker-related changes are made: + +```bash +docker compose config +``` diff --git a/docker-compose.yml b/docker-compose.yml index d3658f2..f4e584d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,11 @@ services: build: context: . dockerfile: Dockerfile + target: runtime + cache_from: + - type=local,src=.docker/buildx-cache + cache_to: + - type=local,dest=.docker/buildx-cache,mode=max image: momswap-backend:latest container_name: momswap-backend-api environment: @@ -11,7 +16,37 @@ services: ports: - "8080:8080" restart: unless-stopped + + api-dev: + profiles: ["dev"] + build: + context: . + dockerfile: Dockerfile + target: dev + cache_from: + - type=local,src=.docker/buildx-cache + cache_to: + - type=local,dest=.docker/buildx-cache,mode=max + image: momswap-backend:dev + container_name: momswap-backend-api-dev + environment: + ADDR: ":8080" + ADMIN_PUBLIC_KEY: "${ADMIN_PUBLIC_KEY:-}" + ports: + - "8080:8080" + restart: unless-stopped develop: watch: + - action: sync+restart + path: ./web + target: /src/web + - action: sync+restart + path: ./internal + target: /src/internal + - action: sync+restart + path: ./cmd + target: /src/cmd - action: rebuild - path: . + path: ./go.mod + - action: rebuild + path: ./Dockerfile diff --git a/docs/typescript-frontend-integration.md b/docs/typescript-frontend-integration.md new file mode 100644 index 0000000..79458d2 --- /dev/null +++ b/docs/typescript-frontend-integration.md @@ -0,0 +1,99 @@ +# TypeScript Frontend Integration Guide + +This document explains how frontend developers should integrate with the backend through the reusable TypeScript client at `libs/geo-api-client`. + +## Goals + +- Keep cryptographic signing logic in one place. +- Avoid duplicating API request code in frontend apps. +- Use a consistent local key storage format across projects. + +## Client package location + +- Source: `libs/geo-api-client/src` +- Entry point: `libs/geo-api-client/src/index.ts` +- Build output (browser ESM): `libs/geo-api-client/dist/index.js` + +## Build and test the client + +```bash +cd libs/geo-api-client +bun install +bun test +bun run build +``` + +## Public API (current) + +### Class: `GeoApiClient` + +Constructor: + +- `new GeoApiClient(baseUrl, storage, storageKey?)` + +Key methods: + +- `ensureKeysInStorage()` +- `getStoredKeys()` +- `importKeys(keys)` +- `exportKeys()` +- `setAccessToken(token)` +- `createChallenge(publicKey)` +- `loginWithSignature(publicKey, privateKey)` +- `createInvitation(payload, inviterPrivateKey)` +- `registerWithInvitation(...)` +- `listCollections()` +- `createCollection(name)` +- `listFeatures(collectionId)` +- `createPointFeature(collectionId, lon, lat, properties)` + +## Recommended integration flow + +1. Create one `GeoApiClient` instance per backend base URL. +2. Call `ensureKeysInStorage()` when app initializes. +3. Use `loginWithSignature()` to obtain and set a bearer token. +4. Call collection/feature methods after authentication. +5. Use `importKeys`/`exportKeys` in profile settings UX. + +## Example (TypeScript app) + +```ts +import { GeoApiClient } from "../libs/geo-api-client/dist/index.js"; + +const storage = window.localStorage; + +const storageLike = { + getItem: (key: string) => storage.getItem(key), + setItem: (key: string, value: string) => storage.setItem(key, value), + removeItem: (key: string) => storage.removeItem(key), +}; + +const client = new GeoApiClient("http://localhost:8080", storageLike); + +const keys = await client.ensureKeysInStorage(); +await client.loginWithSignature(keys.publicKey, keys.privateKey); + +const created = await client.createCollection("My Places"); +await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" }); +const features = await client.listFeatures(created.id); +console.log(features); +``` + +## Security notes + +- Private keys are currently stored in browser storage via the selected storage adapter. +- If your frontend has stronger security requirements, wrap the storage adapter with your own encryption/decryption layer before calling `setItem`/`getItem`. +- Never send private keys to the backend. + +## No-build frontend compatibility + +For no-bundler apps, import the built ESM file: + +```html + +``` + +The backend itself serves static UI at `/web/`, but this library can be consumed by any frontend runtime that supports `fetch`, `TextEncoder`, and ES modules. diff --git a/internal/http/handlers.go b/internal/http/handlers.go index 9db8eea..0348330 100644 --- a/internal/http/handlers.go +++ b/internal/http/handlers.go @@ -21,6 +21,8 @@ func NewAPI(svc *app.Service) *API { func (a *API) Routes() http.Handler { mux := http.NewServeMux() + staticFiles := http.FileServer(http.Dir("web")) + mux.HandleFunc("GET /healthz", a.health) mux.HandleFunc("POST /v1/auth/challenge", a.createChallenge) mux.HandleFunc("POST /v1/auth/login", a.login) @@ -34,6 +36,9 @@ func (a *API) Routes() http.Handler { mux.HandleFunc("GET /v1/collections/{id}/features", a.listFeatures) mux.HandleFunc("DELETE /v1/features/{id}", a.deleteFeature) + mux.Handle("/web/", http.StripPrefix("/web/", staticFiles)) + mux.Handle("/", http.RedirectHandler("/web/", http.StatusTemporaryRedirect)) + return withCORS(mux) }