| Layer | Technology | Notes |
|---|---|---|
| HTTP Server | Go + chi router |
Serves SSR pages + JSON API |
| Templates | Go html/template |
Login, profile, canvas pages |
| Client JS | TypeScript (esbuild + tsc) | Canvas app + minimal fetch() clients |
| Primary DB | Firebase Firestore | Users, projects, gallery, NFTs |
| Auth | Firebase Auth (client) + Firebase Admin SDK (server) | Token verification on every API call |
| SQL DB | PostgreSQL (via pgx/v5) |
Sessions, rate limits, audit logs |
| Migrations | Goose (pressly/goose/v3) |
PostgreSQL schema management |
| NFT Network | Hiero Go SDK (hiero-ledger/hiero-go-sdk) |
Token minting, transfers, marketplace |
| NFT Local Testing | Solo (solo.hiero.org) ≥ 0.54.0 |
Local Hiero network for dev/test |
| Testing | testing + testify (stretchr/testify) |
Go tests; Firebase Emulator + Solo for integration |
| Task Runner | Taskfile.yml (taskfile.dev) |
Build, test, migrate, docker, deploy |
| Logging | log/slog (stdlib) |
Structured request/app logging |
| Local Dev | Docker Compose | Go app + Postgres + Firebase Emulator + Solo |
| Preview | Firebase Hosting preview channel + Cloud Run | Same Docker image as local |
| Production | Firebase Hosting (CDN) + Cloud Run (Go) + Cloud SQL Postgres | Scales to zero |
| Dependency | Version |
|---|---|
| Go | latest (1.23+) |
| Node | 24 (Firebase Emulator container) |
| Solo (Hiero) | ≥ 0.54.0 |
| PostgreSQL | 16 |
| esbuild | latest (via npm) |
| tsc | latest (via npm) |
| Firebase JS SDK | v11.2.0 (client) |
| Firebase Admin SDK | firebase.google.com/go/v4 |
Keep as much of the look and feel of the profile page and paintbar canvas app as possible. CSS and visual design are preserved during migration. Go templates reproduce the existing HTML structure closely.
┌──────────────────────────────────────────────────────────────────────┐
│ ENVIRONMENTS │
├───────────────────┬────────────────────┬─────────────────────────────┤
│ LOCAL (Docker) │ PREVIEW (Firebase) │ PRODUCTION (Firebase) │
├───────────────────┼────────────────────┼─────────────────────────────┤
│ Go app container │ Cloud Run container │ Cloud Run container │
│ │ │ │
│ Postgres container │ Cloud SQL Postgres │ Cloud SQL Postgres │
│ (port 5432) │ (preview instance) │ (production instance) │
│ │ │ │
│ Firebase Emulator │ Firebase preview │ Firebase production │
│ - Auth (9099) │ channel │ project (paintbar-7f887) │
│ - Firestore (8080) │ (paintbar-7f887) │ │
│ - UI (4000) │ │ │
│ │ │ │
│ Solo (Hiero local) │ Hiero testnet │ Hiero mainnet │
│ (port 50211) │ │ │
│ │ │ │
│ ENV=local │ ENV=preview │ ENV=production │
│ Goose seeds test │ Goose migrates on │ Goose migrates via CI/CD │
│ data │ deploy │ │
└───────────────────┴────────────────────┴─────────────────────────────┘
User's Browser
│
▼
┌─────────────────────┐
│ Firebase Hosting │
│ (Global CDN) │
│ │
│ Serves directly: │ ← CSS, JS bundles, images
│ • /static/** │
│ • /favicon.ico │
│ │
│ Rewrites to │
│ Cloud Run: │
│ • / │ ← Login page (Go template)
│ • /profile │ ← Profile page (Go template)
│ • /canvas │ ← Canvas page (Go template)
│ • /api/** │ ← All API endpoints
└────────┬────────────┘
│ HTTPS rewrite
▼
┌─────────────────────┐
│ Cloud Run (Go) │
│ │
│ ┌───────────────┐ │
│ │ chi Router │ │
│ │ + Middleware │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Handlers │ │
│ │ (API + Pages) │ │
│ └───┬───────┬───┘ │
│ │ │ │
│ ┌───▼──┐ ┌──▼────┐ │
│ │ Fire-│ │Cloud │ │
│ │ store│ │SQL PG │ │
│ └──────┘ └───────┘ │
│ │ │
│ ┌───▼──────────┐ │
│ │ Hiero SDK │ │
│ │ (NFT ops) │ │
│ └──────────────┘ │
└─────────────────────┘
paintbar/
├── cmd/
│ └── server/
│ └── main.go # Entry point, bootstrap, graceful shutdown
├── internal/
│ ├── config/
│ │ └── config.go # Multi-env config (local/preview/prod)
│ ├── middleware/
│ │ ├── auth.go # Firebase token verification
│ │ ├── ratelimit.go # Postgres-backed rate limiting
│ │ ├── logging.go # slog request logging
│ │ ├── cors.go # CORS headers
│ │ ├── recovery.go # Panic recovery
│ │ ├── security.go # CSP, CSRF, secure headers
│ │ └── middleware_test.go
│ ├── handler/
│ │ ├── pages.go # SSR page handlers
│ │ ├── auth.go # POST /api/auth/session, logout
│ │ ├── profile.go # GET/PUT /api/profile
│ │ ├── projects.go # CRUD /api/projects
│ │ ├── gallery.go # GET/POST /api/gallery
│ │ ├── nfts.go # GET /api/nfts, mint, list
│ │ └── *_test.go
│ ├── service/
│ │ ├── auth.go # Token verification logic
│ │ ├── user.go # Profile business logic
│ │ ├── project.go # Project business logic
│ │ ├── gallery.go # Gallery business logic
│ │ ├── nft.go # NFT business logic (Hiero SDK)
│ │ └── *_test.go
│ ├── repository/
│ │ ├── firestore.go # Firestore client init + wrapper
│ │ ├── postgres.go # pgx pool init + health check
│ │ ├── user.go # User Firestore CRUD
│ │ ├── project.go # Project Firestore CRUD
│ │ ├── gallery.go # Gallery Firestore CRUD
│ │ ├── nft.go # NFT Firestore CRUD
│ │ ├── session.go # Session Postgres CRUD
│ │ └── *_test.go
│ └── model/
│ ├── user.go # User struct + validation
│ ├── project.go # Project struct + validation
│ ├── gallery.go # Gallery struct + validation
│ └── nft.go # NFT struct + validation (Hiero types)
├── migrations/
│ ├── 001_create_sessions.sql
│ ├── 002_create_rate_limits.sql
│ └── 003_create_audit_logs.sql
├── web/
│ ├── templates/
│ │ ├── layouts/
│ │ │ └── base.html
│ │ ├── pages/
│ │ │ ├── login.html
│ │ │ ├── profile.html
│ │ │ └── canvas.html
│ │ └── partials/
│ │ ├── nav.html
│ │ └── footer.html
│ ├── static/
│ │ ├── images/ # Migrated from public/images/
│ │ ├── styles/ # Migrated from public/styles/
│ │ └── dist/ # esbuild + tsc output
│ └── ts/
│ ├── canvas/
│ │ ├── app.ts
│ │ ├── canvasManager.ts
│ │ ├── toolManager.ts
│ │ ├── basicTools.ts
│ │ ├── objectTools.ts
│ │ ├── actionTools.ts
│ │ ├── genericTool.ts
│ │ └── save.ts
│ ├── profile/
│ │ └── profile.ts # fetch()-based profile client
│ ├── auth/
│ │ └── login.ts # Firebase Auth client + session POST
│ └── tsconfig.json
├── testdata/
│ └── firestore/ # Firebase Emulator seed data
├── Dockerfile # Multi-stage: Go build + esbuild + tsc + runtime
├── docker-compose.yml # app + postgres + firebase-emulator + solo
├── Taskfile.yml # Task runner targets
├── firebase.json # Hosting rewrites to Cloud Run
├── firestore.rules # Firestore security rules
├── firestore.indexes.json # Firestore composite indexes
├── go.mod
├── go.sum
├── .env.example # All env vars documented
├── .gitignore # Updated for Go/TS/Docker
└── docs/
├── migration-plan.md # Reference copy of this plan
└── database-schema.md # Updated schema docs
Goal: Project structure, all dependencies, Docker, Taskfile — task dev runs a server.
- Initialize Go module:
go mod init github.com/pandasWhoCode/paintbar - Add Go dependencies:
github.com/go-chi/chi/v5(router)github.com/jackc/pgx/v5(Postgres)github.com/pressly/goose/v3(migrations)github.com/stretchr/testify(testing)firebase.google.com/go/v4(Firebase Admin SDK)google.golang.org/api(Google API client)github.com/hiero-ledger/hiero-go-sdk(NFT — added to go.mod, not used until Phase 4+)
- Create full directory structure (all dirs listed above)
- Create
Taskfile.yml:task build— compile Go binarytask run— run dev servertask test— run all Go teststask migrate/task migrate-down/task migrate-status— Goosetask ts-build— esbuild + tsc TypeScript compilationtask dev— run server + watch TStask docker-up/task docker-down— Docker Composetask deploy-preview— Firebase preview channel + Cloud Runtask deploy-prod— Production deploytask lint— Go vet + staticcheck
- Create
Dockerfile(multi-stage):- Stage 1: Go build (
golang:1.23-alpine) - Stage 2: esbuild + tsc TS build (
node:24-alpinewith esbuild + tsc) - Stage 3: Runtime (
alpine:latest— minimal, just the binary + static assets)
- Stage 1: Go build (
- Create
docker-compose.yml(4 services):app— Go server (builds from Dockerfile, depends on postgres + firebase + solo)postgres— PostgreSQL 16 (health check, persistent volume, init script)firebase— Firebase Emulator Suite (Node 24 with firebase-tools, Auth + Firestore)solo— Hiero Solo ≥ 0.54.0 local network (for NFT testing)
- Create
.env.examplewith all env vars:ENV=local PORT=8080 DATABASE_URL=postgres://paintbar:paintbar@localhost:5432/paintbar?sslmode=disable FIREBASE_PROJECT_ID=paintbar-7f887 FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-service-account.json FIRESTORE_EMULATOR_HOST=localhost:8080 FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 HIERO_NETWORK=local HIERO_OPERATOR_ID= HIERO_OPERATOR_KEY= - Install TS dev deps:
npm install --save-dev typescript esbuild @types/node, createweb/ts/tsconfig.json(strict, ES2022, DOM lib) - Update
.gitignore: addbin/,web/static/dist/,firebase-service-account.json,*.db, Docker volumes, Go test cache - Tests:
go build ./...compiles,task buildsucceeds
Goal: Running Go HTTP server with middleware, config, chi router, graceful shutdown.
internal/config/config.go:- Config struct with all env vars
Load()function reads from environment- Environment detection:
local→ set emulator hosts,preview/production→ use real Firebase - Validation: required fields checked per environment
cmd/server/main.go:- Load config
- Init Postgres connection pool
- Init Firebase Admin app
- Init Firestore client
- Register chi routes
- Start HTTP server with
context-based graceful shutdown (SIGINT,SIGTERM)
internal/middleware/:logging.go—slogstructured request logging (method, path, status, duration, IP)ratelimit.go— Token bucket rate limiter (in-memory for Phase 1, Postgres-backed in Phase 2)cors.go— CORS headers for API routesrecovery.go— Panic recovery → 500 JSON responsesecurity.go— CSP, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security
- Chi router setup:
GET /→ login page handlerGET /profile→ profile page handlerGET /canvas→ canvas page handlerGET /static/*→http.FileServerforweb/static//api/*group with auth middleware
- Tests: Server startup/shutdown, config loading, middleware ordering, 404 handling
Goal: Postgres connected, Goose migrations running, session/rate-limit/audit tables ready.
migrations/001_create_sessions.sql:-- +goose Up CREATE TABLE sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, ip_address INET, user_agent TEXT, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); -- +goose Down DROP TABLE sessions;
migrations/002_create_rate_limits.sql:-- +goose Up CREATE TABLE rate_limits ( id BIGSERIAL PRIMARY KEY, key TEXT NOT NULL, window_start TIMESTAMPTZ NOT NULL, request_count INT NOT NULL DEFAULT 1, UNIQUE(key, window_start) ); CREATE INDEX idx_rate_limits_key_window ON rate_limits(key, window_start); -- +goose Down DROP TABLE rate_limits;
migrations/003_create_audit_logs.sql:-- +goose Up CREATE TABLE audit_logs ( id BIGSERIAL PRIMARY KEY, user_id TEXT, action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, details JSONB, ip_address INET, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at); -- +goose Down DROP TABLE audit_logs;
internal/repository/postgres.go—pgxpoolinit, health check, connection retryinternal/repository/session.go—Create,GetByToken,DeleteByToken,DeleteByUserID,CleanupExpired- Update
internal/middleware/ratelimit.go→ Postgres-backed - Goose embedded in
cmd/server/main.go— auto-migrate on startup whenENV=local - Taskfile:
task migrate,task migrate-down,task migrate-status - Tests: Migration up/down idempotency, session CRUD, rate limit logic, expired session cleanup
Goal: Go server verifies Firebase ID tokens and reads/writes Firestore.
internal/repository/firestore.go:NewFirebaseApp()— init Firebase Admin appENV=local→option.WithoutAuthentication()+ emulator env varsENV=preview/production→option.WithCredentialsFile(serviceAccountPath)
NewFirestoreClient()— get Firestore client from Firebase app- Health check: ping Firestore
internal/service/auth.go:VerifyIDToken(ctx, idToken)→ returns UID, email, claims- Uses
auth.Clientfrom Firebase Admin SDK - In local mode, connects to Auth emulator automatically (via
FIREBASE_AUTH_EMULATOR_HOST)
internal/middleware/auth.go:- Extracts
Authorization: Bearer <token>header - Calls
auth.VerifyIDToken() - Injects user info into
context(context.WithValue) - Returns
401 Unauthorizedfor invalid/expired tokens - Skip list: login page, static assets, health check endpoint
- Extracts
- Docker Compose
firebaseservice:- Node 24 image with
firebase-toolsinstalled - Runs
firebase emulators:start --only auth,firestore --import /testdata - Mounts
testdata/firestore/for seed data - Exposes: 4000 (Emulator UI), 8080 (Firestore), 9099 (Auth)
- Node 24 image with
- Tests: Token verification (emulator), middleware auth/reject flow, Firestore client connection
Goal: Go structs for all entities, Firestore CRUD, repository interfaces.
internal/model/user.go:Userstruct (UID, Email, Username, DisplayName, Bio, Location, Website, GithubUrl, TwitterHandle, BlueskyHandle, InstagramHandle, HbarAddress, CreatedAt, UpdatedAt)Validate(),Sanitize()methodsUserUpdatestruct for partial updates
internal/model/project.go:Projectstruct (ID, UserID, Name, Description, ImageData, ThumbnailData, Width, Height, IsPublic, Tags, CreatedAt, UpdatedAt)- Validation, sanitization
internal/model/gallery.go:GalleryItemstruct
internal/model/nft.go:NFTstruct — includes Hiero-specific fields (TokenID, SerialNumber, TransactionID)- Note: Hiero SDK types integrated here after studying
hiero-go-sdkand Solo docs
- Repository interfaces (for testability):
type UserRepository interface { GetByID(ctx context.Context, uid string) (*model.User, error) Create(ctx context.Context, user *model.User) error Update(ctx context.Context, uid string, update *model.UserUpdate) error ClaimUsername(ctx context.Context, uid string, username string) error }
internal/repository/user.go— Firestore implementation ofUserRepositoryClaimUsernameuses Firestore transaction (atomic check + write onusernamescollection)
internal/repository/project.go—ProjectRepositoryinterface + Firestore implListwith pagination (startAfter cursor),Countvia aggregation
internal/repository/gallery.go,nft.go— same pattern- Tests: Repository CRUD against Firebase Emulator, model validation, username claiming race conditions
Goal: Authorization, validation, business rules — fully testable with mocked repos.
internal/service/user.go:GetProfile(ctx, uid)— fetch + sanitize for responseUpdateProfile(ctx, uid, input)— validate, sanitize, normalize handles (strip@), validate URLsClaimUsername(ctx, uid, username)— format validation (^[a-z0-9_-]{3,30}$) + uniqueness
internal/service/project.go:ListProjects(ctx, uid, page)— paginated, ownership enforcedGetProject(ctx, uid, projectID)— ownership or isPublic checkCreateProject(ctx, uid, input)— validate, set timestampsUpdateProject(ctx, uid, projectID, input)— ownership checkDeleteProject(ctx, uid, projectID)— ownership checkCountProjects(ctx, uid)— true count via Firestore aggregation
internal/service/gallery.go— share to gallery, list, ownershipinternal/service/nft.go:- Hiero SDK integration: mint NFT, list for sale, transfer
- Uses
hiero-go-sdkfor network interactions - Local: connects to Solo network
- Preview: Hiero testnet
- Production: Hiero mainnet
- Implemented last — after thorough study of Hiero SDK + Solo docs
- Tests: Business logic with testify mocks, edge cases, authorization failures
Goal: RESTful JSON API endpoints wired to services.
internal/handler/auth.go:POST /api/auth/session— Exchange Firebase ID token for server sessionPOST /api/auth/logout— Destroy session
internal/handler/profile.go:GET /api/profile— Get current user's profilePUT /api/profile— Update profilePUT /api/profile/username— Claim usernamePOST /api/profile/reset-password— Trigger password reset email
internal/handler/projects.go:GET /api/projects— List (paginated)GET /api/projects/{id}— Single projectPOST /api/projects— CreatePUT /api/projects/{id}— UpdateDELETE /api/projects/{id}— DeleteGET /api/projects/count— True count
internal/handler/gallery.go:GET /api/gallery— List user's gallery itemsPOST /api/gallery— Share to gallery
internal/handler/nfts.go:GET /api/nfts— List user's NFTsPOST /api/nfts/mint— Mint NFT (Hiero SDK)PUT /api/nfts/{id}/list— List/delist for sale- Implemented last — after Hiero SDK study
- Response format:
{ "data": { ... }, "meta": { "count": 42, "page": 1 }, "error": null } - Tests:
httptesthandler tests, request validation, error responses, auth enforcement
Goal: Server-rendered HTML replaces static HTML files. Preserve existing UI look and feel.
web/templates/layouts/base.html—<html>,<head>, nav, footer, script/style includesweb/templates/partials/nav.html— Navigation bar (conditional auth state)web/templates/partials/footer.html— Footer with copyrightweb/templates/pages/login.html— Login/signup page (migrated frompublic/login.html)web/templates/pages/profile.html— Profile sidebar + grids (migrated frompublic/profile.html)web/templates/pages/canvas.html— Full canvas app (migrated frompublic/index.html)internal/handler/pages.go:GET /→ render login (redirect to/profileif valid session)GET /profile→ render profile (redirect to/if no session)GET /canvas→ render canvas (requires session)- Custom 404 handler
- Migrate assets:
public/styles/→web/static/styles/(CSS preserved as-is)public/images/→web/static/images/
- Security headers injected: CSP, CSRF tokens in forms
- Tests: Template rendering, redirect logic, CSRF token presence
Goal: All client JS → TypeScript, remove all direct Firestore access from client.
- Canvas app (
web/ts/canvas/):app.ts←app.js(PaintBar class, 57KB)canvasManager.ts←canvasManager.jstoolManager.ts←toolManager.jsbasicTools.ts←basicTools.jsobjectTools.ts←objectTools.jsactionTools.ts←actionTools.jsgenericTool.ts←genericTool.jssave.ts←save.js- Remove all Firebase imports from canvas code
- Add TypeScript interfaces for all tool options, canvas state, drawing events
- Auth client (
web/ts/auth/login.ts):- Firebase Auth client SDK (email+password login/signup)
- On success: get ID token →
POST /api/auth/session→ redirect to/profile - Error handling with typed error codes
- Profile client (
web/ts/profile/profile.ts):- Replace all
onSnapshot/Firestore calls withfetch('/api/...') - Keep DOM manipulation (safe rendering, sanitization)
- Typed API response interfaces
- Replace all
web/ts/tsconfig.json:strict: true,target: "ES2022",lib: ["ES2022", "DOM"]moduleResolution: "bundler"
- esbuild + tsc build pipeline:
npx esbuild web/ts/canvas/app.ts web/ts/auth/login.ts web/ts/profile/profile.ts \ --outdir web/static/dist --splitting --minify npx tsc --project web/ts/tsconfig.json --noEmit
- Tests:
tsc --noEmit(type checking), compilation success
Goal: End-to-end validation, remove legacy code, harden, document, deploy.
- Integration tests (Docker Compose + all emulators):
- Login → session → profile CRUD → project CRUD → gallery share → logout
- Rate limiting enforcement
- Session expiry
- NFT minting (against Solo local network)
- Remove legacy Node.js files:
server.js,package.json,package-lock.json,jest.config.jsnode_modules/,tests/public/scripts/,public/styles/,public/*.htmlpublic/directory entirely (replaced byweb/)
- Update
firebase.jsonfor Cloud Run rewrites:{ "hosting": { "public": "web/static", "rewrites": [ { "source": "**", "run": { "serviceId": "paintbar", "region": "us-central1" } } ] } } - Security hardening:
- CSP headers (Content-Security-Policy)
- CSRF protection on all state-changing endpoints
- Secure session cookies (HttpOnly, Secure, SameSite=Strict)
- Request body size limits
- Input sanitization at service layer
- SQL injection prevention (pgx parameterized queries)
- Firestore rules as defense-in-depth (secondary to Go API authorization)
- Deployment setup:
- Taskfile:
task deploy-preview(Cloud Run + Firebase Hosting preview channel) - Taskfile:
task deploy-prod(Cloud Run + Firebase Hosting production) - Cloud SQL Postgres setup guide
- CI/CD pipeline outline (GitHub Actions)
- Taskfile:
- Update docs: README, CONTRIBUTING,
docs/database-schema.md - Final test pass:
task test+task docker-up && task test-integration+task ts-check
Phase 0 (Scaffolding) ─────────────────────────────────────────────►
│
▼
Phase 1 (Go Server) ──────────────────────────────────────────────►
│
├──► Phase 2 (Postgres + Goose) ─────────────────────────►
│ (parallel)
└──► Phase 3 (Firebase Admin SDK) ─────────────────────────►
│
▼
Phase 4 (Models + Repos) ─────────────────────────────────►
│ NFT portions deferred until
▼ Hiero SDK + Solo studied
Phase 5 (Services) ───────────────────────────────────────►
│
▼
Phase 6 (API Handlers) ───────────────────────────────────►
│
├──► Phase 7 (Go Templates) ────────────────────────►
│ (parallel)
└──► Phase 8 (TypeScript) ────────────────────────►
│
▼
Phase 9 (Integration + Cleanup + Deploy) ───────►
NFT-specific work (Hiero SDK integration in Phases 4-6) is deferred within each phase until after thorough study of github.com/hiero-ledger/hiero-go-sdk and solo.hiero.org. Non-NFT functionality (users, projects, gallery) proceeds independently.