main·c6caae5·just now

Deployed project

GalaxyVoyagers.com

Migrating to AWS · Phase 1 of 4

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.com

Technology Stack

The 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.

Next.js App RouterReactTypeScriptApollo ClientGogqlgenGraphQL subscriptionsgRPC + ProtobufPostgreSQLMongoDBRedisRabbitMQOpenAIImage generationPrompt engineeringDockerGitHub ActionsAWSEKSKarpenterGraviton (ARM64)TerraformAurora Serverless v2DocumentDBElastiCache (Valkey)Amazon MQALBRoute 53ACMIRSAExternal Secretsghcr.io

Architecture

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.

How the Generative AI Works

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.

Text Generation: Context-Injected Prompting

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: Layered Style Composition

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.

Prompt Engineering Takeaways

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.

Context injection over raw passthrough

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.

Relevance-scoped context

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.

Layered, compositional prompts

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.

Execution model matched to cost

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.

Why GraphQL Was The Right Boundary

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.

Connected worldbuilding data

Stories connect to scenes, characters, locations, organizations, conflicts, roles, ships, generated images, and discussion content.

One declarative UI query

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.

Gateway-owned composition

The Go GraphQL gateway resolves fields across backend services and datastores while keeping the browser API explicit and stable.

Subscriptions fit live generation

GraphQL subscriptions support streaming story suggestions and async creation flows without adding a second frontend API model.

Production AWS Migration

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.

Self-hosted → AWS managed

Today (homelab)AWS managed targetNotes
PostgreSQL (story, auth)Aurora PostgreSQL Serverless v2One cluster, two databases; autoscales from a low ACU floor.
MongoDB (chat)Amazon DocumentDBCRUD-only chat usage; TLS required on the connection.
RedisElastiCache Serverless (Valkey)Pay-per-use, scales to a low idle floor.
RabbitMQAmazon MQ for RabbitMQSingle-instance broker for cost; cluster deployment for HA later.
Static AWS keys in podsIRSA (scoped IAM roles)S3 image access without long-lived credentials.
sealed-secretsSecrets Manager + External Secrets OperatorSynced into native Kubernetes Secrets via IRSA.
nginx ingressALB + ACM + external-dnsRoute 53 record for api.galaxyvoyagers.com.

Migration phases

Phase 1 — Foundation

In progress

Remote state, VPC, EKS + Graviton baseline node group, Karpenter, and core cluster add-ons (LB Controller, external-dns, External Secrets Operator, metrics-server).

Phase 2 — Data layer

Upcoming

Aurora, DocumentDB, ElastiCache, and Amazon MQ; Secrets Manager entries synced into the cluster by External Secrets.

Phase 3 — App layer

Upcoming

IRSA roles, an EKS kustomize overlay pointing services at the managed datastores, and the ALB Ingress with ACM + external-dns.

Phase 4 — Cutover

Upcoming

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.

Observability

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.

Engineering Focus

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).