Deployed project
GalaxyVoyagers is a collaborative sci-fi worldbuilding platform for building stories, scenes, characters, organizations, locations, ships, conflicts, and supporting media. It is a separate production deployment that demonstrates how I design a full-stack application around a connected domain instead of treating each screen as an isolated CRUD form.
The live site uses a Next.js frontend backed by a Go GraphQL gateway. Behind that gateway, Go services communicate over gRPC, store domain data in PostgreSQL and MongoDB, use Redis for shared runtime state, and send async work through RabbitMQ for AI-assisted story and image generation flows.
The site is served today from a self-hosted k3s homelab and is actively migrating to a production AWS deployment — the target architecture is documented below.
Open GalaxyVoyagers.comThe project is intentionally polyglot at the system boundary but conservative inside each service: TypeScript and Apollo Client in the browser, Go and gqlgen at the API gateway, protobuf-defined gRPC contracts between services, and proven datastores selected for the access pattern they serve.
The browser talks to one GraphQL entry point for queries, mutations, and subscriptions. The gateway owns backend composition: it calls the story, chat, auth, image, story-generation, and Stripe services over gRPC, while async generation work moves through RabbitMQ and streams results back to the UI.
GalaxyVoyagers has two generative capabilities with deliberately opposite execution models, and both are driven by prompt engineering rather than by passing raw user text to a model. Text generation runs synchronously and streams results token-by-token, so the writer sees prose appear immediately. Image generation is expensive, so it is decoupled onto an asynchronous queue with retries. In both cases the interesting work happens before the model call: the service assembles a structured prompt from the surrounding worldbuilding data.
When a writer asks for a scene description, the request reaches the storygen service over gRPC. Before calling the model, storygen pulls the entities related to that scene — characters, locations, conflicts, organizations, and ship casualties — from a Redis cache, falling back to PostgreSQL on a miss. It folds them into a single context-injected prompt, then streams the model's output back token-by-token over a gRPC server stream.
The prompt itself is built in layers. A fixed task instruction comes first, then the writer's seed text, then a context block populated with the related entities:
// storygen assembles the scene prompt by injecting related entities (Go) prompt := "I need a description of this scene in the form of an essay.\n" prompt += req.Text // the writer's seed text prompt += "\nFor context:\n" prompt += "The following characters are involved in this scene: " + characters prompt += "The scene takes place at the following locations: " + locations prompt += "This scene is associated with the following conflicts: " + conflicts prompt += "The following organizations are involved in this scene: " + orgs // characters are filtered to ONLY the organizations involved in this scene, // and every entity is fetched cache-first (Redis, 10-min TTL) -> Postgres.
The result is a prompt grounded in the specific corner of the universe the writer is editing, which keeps generated prose consistent with established characters and places instead of inventing contradictions.
Image generation is slow and expensive, so it is decoupled from the request path. The image service accepts the job over gRPC and enqueues it on RabbitMQ. A consumer composes the final prompt, calls the image model, uploads the PNG to object storage, records metadata in PostgreSQL, and notifies the gateway with a signed URL through an HTTP callback. Failed jobs are retried up to three times before moving to a dead-letter queue.
The prompt is assembled by a style composer from a three-part template: a global art-direction base, a per-entity-type overlay that reframes the composition, and the user's subject text.
The entire visual identity lives in a YAML configmap loaded at startup, so the art direction can be tuned without a code change:
# services/image/style/templates.yaml
version: 1
format: "{base}. {overlay}. Subject: {user_prompt}"
base: |-
Anime sci-fi superhero aesthetic. Cel-shaded with crisp ink lines and
vibrant saturated palette. Dramatic rim lighting against deep space or
neon-lit backdrops. High detail, dynamic composition, painterly shading.
entities:
characters:
overlay: "Hero portrait, three-quarter angle, expressive pose, costume detail emphasized."
ships:
overlay: "Exterior hero shot, 3/4 angle, sense of scale, engines glowing, motion-blurred star field."
scenes:
overlay: "Wide cinematic establishing shot, atmospheric perspective, characters small in frame."A request to illustrate a character therefore composes to a single prompt — the base style, the character overlay, then Subject: <the user's description> — giving every generated image a consistent, art-directed look across the platform.
Across both generation paths, the same prompt-engineering principles recur: ground the model in real domain data, keep that data relevant and cheap to fetch, compose prompts in layers, and match the execution model to the cost of the work.
Generation never sends only the user's text to the model. The service assembles a prompt from related worldbuilding entities so output stays consistent with the established universe.
Injected entities are fetched cache-first (Redis, 10-minute TTL) with a Postgres fallback, and scene characters are filtered to only the organizations involved in that scene — focused prompts, not context dumps.
Images compose a global art-direction base, a per-entity-type overlay, and the user's subject. Text layers a task instruction, the writer's seed text, and a structured context block.
Cheap, latency-sensitive text streams back token-by-token over gRPC. Expensive image jobs are queued through RabbitMQ with retries and a dead-letter queue.
GalaxyVoyagers is not a flat resource catalog. A useful screen often needs a nested view: a story, its ordered scenes, the characters and locations in each scene, related organizations and conflicts, generated images, and discussion context. GraphQL fits that shape because the UI can request the exact graph it needs in one operation.
With a REST-only browser API, that same screen would tend to become a chain of dependent requests: fetch the story, fetch scenes, fetch the entities attached to each scene, fetch media, then fetch comments. The GraphQL gateway moves that composition into the backend, where it can resolve nested fields through service calls and datastore access without forcing the browser to coordinate every step.
Stories connect to scenes, characters, locations, organizations, conflicts, roles, ships, generated images, and discussion content.
The frontend can ask for the nested shape a screen needs instead of fetching a story, then scenes, then related entities, then media through chained browser calls.
The Go GraphQL gateway resolves fields across backend services and datastores while keeping the browser API explicit and stable.
GraphQL subscriptions support streaming story suggestions and async creation flows without adding a second frontend API model.
The migration moves GalaxyVoyagers from a self-hosted homelab onto a managed, autoscaling AWS deployment without rewriting application services. Managed EKS runs the existing manifests; Karpenter provisions spot capacity over a small Graviton (ARM64) on-demand baseline, consolidating and scaling to zero extra nodes when idle. ARM instances run the Go services at roughly 20% lower cost than x86.
| Today (homelab) | AWS managed target | Notes |
|---|---|---|
| PostgreSQL (story, auth) | Aurora PostgreSQL Serverless v2 | One cluster, two databases; autoscales from a low ACU floor. |
| MongoDB (chat) | Amazon DocumentDB | CRUD-only chat usage; TLS required on the connection. |
| Redis | ElastiCache Serverless (Valkey) | Pay-per-use, scales to a low idle floor. |
| RabbitMQ | Amazon MQ for RabbitMQ | Single-instance broker for cost; cluster deployment for HA later. |
| Static AWS keys in pods | IRSA (scoped IAM roles) | S3 image access without long-lived credentials. |
| sealed-secrets | Secrets Manager + External Secrets Operator | Synced into native Kubernetes Secrets via IRSA. |
| nginx ingress | ALB + ACM + external-dns | Route 53 record for api.galaxyvoyagers.com. |
Remote state, VPC, EKS + Graviton baseline node group, Karpenter, and core cluster add-ons (LB Controller, external-dns, External Secrets Operator, metrics-server).
Aurora, DocumentDB, ElastiCache, and Amazon MQ; Secrets Manager entries synced into the cluster by External Secrets.
IRSA roles, an EKS kustomize overlay pointing services at the managed datastores, and the ALB Ingress with ACM + external-dns.
Data migration, flip the api.galaxyvoyagers.com Route 53 record to the ALB, verify, then decommission the homelab stack.
For a leaner, ephemeral take on the same AWS tools — spin up the portfolio's own services for a demo and tear them down after — see the portfolio AWS deployment.
GalaxyVoyagers ships the same Prometheus / Loki / Grafana observability stack used across this portfolio — Prometheus scrape annotations on pods, Loki log queries, and Grafana dashboards for the story services. The approach is documented in detail in the Observability section.
The project highlights production-oriented backend design and infrastructure: a typed GraphQL boundary, protobuf service contracts, separate persistence models for relational worldbuilding data and document-style discussion data, async job handling for expensive generation work, and a Terraform-defined migration onto autoscaling AWS managed services (EKS with Karpenter and Graviton, Aurora Serverless v2, DocumentDB, IRSA, and External Secrets).