Skip to content

Products

GPU CloudNVIDIA H200 by the hour.
Dedicated ClustersPreviewPrivate, interconnected GPU clusters for sustained workloads.
SandboxesPreviewIsolated, short-lived environments for developers and AI agents.
Serverless GPUPreviewGPU containers that scale with requests, down to zero.
Serverless InferenceOpen models behind one OpenAI-compatible endpoint.
Dedicated EndpointsReserved serving capacity for one model, billed per GPU-hour.
PlaygroundSend a prompt to any model your workspace can reach.
Model DeploymentsPreviewImmutable deployments with instant promote and rollback.
Fine-tuningLoRA fine-tunes on the open models in our catalog.
Model TrainingPreviewFoundation-model training runs, managed end to end.
EvaluationsPreviewBenchmark suites and model comparisons you can rerun.
DatasetsManaged and synthetic training data, versioned and ready to use.
Data ManagementPreviewDatasets, connections and storage, with lineage across the platform.
PipelinesPreviewVersioned DAGs that automate data, training and deployment.
Data RoomsPreviewSecure document sharing with guests, NDAs and an audit trail.
NokuvaPreviewThe AI-native design editor.
TavocPreviewA Rust-native code platform: IDE and desktop apps.
FoltracComing soonMulti-cloud deployment with autonomous operations.
W0rkTreeComing soonVersion control for agent fleets.
Identity & AccessMembers, roles, policies, SSO and audit for every workspace.
Billing & CostUsage, invoices, credits and budgets in one place.
Logs & MonitoringRequest logs, latency and error rates across services.
Project CanvasPreviewSee and change a project's resources on one live canvas.

Pages

Home/
Platform/platform
All products/products
Pricing/pricing
GPU pricing/pricing#compute
Inference services/services/inference
Model training services/services/model-training
Protocol design/services/protocol-design
Research partnerships/services/research-partnerships
Protocols/protocols
Datasets/datasets
Documentationdocs.legionedge.ai
Changelog/changelog
News/news
Research/research
Blog/blog
Articles/articles
Events/events
Startup Program/programs/startup
Ambassador Program/programs/ambassador
Partner Program/programs/partner
Event Program/programs/events
Company/company
Careers/careers
Partners/partners
Contact/contact
Statusstatus.legionedge.ai
Terms of Service/legal/terms
Privacy Policy/legal/privacy

All blog posts

One origin, three consoles: how cloud.legionedge.ai ships

The new consoles run as Next.js zones behind a single origin on GKE. Here's why, and the pipeline that rolls them out on every push to main.

Category
Engineering
Published
Reading time
4 min
Sections
06
Source
Engineering blog
01

Overview

This week the new LegionEdge consoles went live at cloud.legionedge.ai: the cloud console, the IAM app where you sign in and run your organization’s identity, and the admin panel our platform team works from. This post covers how they’re put together and how a commit on main reaches production.

Two constraints shaped the design. Three apps had to share one session, and a deploy couldn’t depend on anyone remembering a manual step.

02

One origin, because of one cookie

Better IAM’s session cookie carries the __Host- prefix. Browsers only accept such a cookie when it is Secure, has Path=/ and sets no Domain, which ties it to a single host. If the cloud console, the IAM app and the admin panel each had their own subdomain, signing in to one would leave you signed out of the other two.

So production is a single origin built from Next.js multi-zones. apps/cloud owns / and every /<your-alias> path. The IAM app is mounted at /iam and the admin app at /admin. Only the cloud app sits behind the load balancer. Its rewrites forward /iam and /admin (pages, API routes and the /_next assets under each zone’s base path) to the other two services inside the cluster. They run as beforeFiles rewrites, so those paths never reach the cloud app’s own organization route.

Sharing one origin also explains why some organization aliases are reserved: iam, admin, api, _next, legionedge, login and signup would collide with top-level paths. Because client components link across zones, the origin is a build argument, and each image set is built for exactly one origin.

The short hostnames are redirects, not separate apps. iam.legionedge.ai, admin.legionedge.ai and app.legionedge.ai forward into cloud.legionedge.ai and keep the path, so iam.legionedge.ai/login lands on cloud.legionedge.ai/iam/login. Each hostname has its own Certificate Manager certificate, so adding one never touches the certificate that serves live traffic.

03

The edge

Traffic arrives at a GKE Gateway, a global external Application Load Balancer on a static IP, with TLS 1.2 or later and a redirect from HTTP to HTTPS. Certificates are authorized through DNS, so they issue before a domain points at the load balancer and a cutover has no gap without a certificate.

Cloud Armor sits in front of the Gateway. It runs the OWASP Core Rule Set 3.3 at its lowest-false-positive sensitivity, along with adaptive DDoS protection, a per-IP rate limit and a tighter budget on the sign-in and sign-up paths. One lesson from launch day: CRS rule 921150, which looks for CR/LF in argument names, matched every Next.js server-action form post, sign-in included. We opted out of that one signature and kept the rest of the protocol-attack rules.

04

From push to rollout

Every push to main starts the Deploy workflow. No credentials are stored in it. GitHub’s OIDC token is exchanged through Workload Identity Federation for a deployer service account that only accepts this repository’s main branch, and no service account keys exist.

The build stage is a matrix of six images: cloud, iam, admin, website, worker and compute. Each builds in its own job with docker buildx bake, using a GitHub Actions cache scoped to that app. Registry tags are immutable, so every tag combines the commit, the run number and the attempt, and a re-run gets a tag of its own. Bake pushes an SBOM and SLSA provenance with each image.

Only one deploy runs at a time. A newer push waits for the current rollout to finish instead of cancelling it. The deploy script reads the build metadata and pins every image in the Kustomize overlay by digest. The tag is only a label, and Kubernetes runs exactly the bytes that were built.

05

Migrations before code

The rollout happens in three phases. First, configuration goes out: ConfigMaps, Secret Manager provider classes, NetworkPolicies and service accounts. Second, a one-off Kubernetes Job runs pnpm db:migrate on the new iam image. That applies every schema’s pending SQL files from packages/*/migrations/<schema>/ in file-name order, each under its own Postgres advisory lock, and records them in that schema’s migrations table. Third, if and only if the Job succeeded, everything else is applied and the script waits on each deployment’s rollout.

When a migration fails, nothing rolls out and the running pods stay as they were. We added this ordering after launch. Our packages also migrate on first use, but only on code paths that open their schema. On the fresh production database, the admin Plans page queried a table that no code path had created yet. Running every schema’s migrations ahead of the rollout removes that whole class of bug, and new code never runs against an old schema.

06

What runs in the cluster

Images are built in a Debian slim stage and run on distroless Node 24 as a non-root user. They have no shell and no package manager, and base images are pinned by digest. Binary Authorization admits only images from our own registry plus the pinned Cloud SQL Auth Proxy. Pods run under the restricted Pod Security standard with a read-only root filesystem, every Linux capability dropped and no service account token mounted.

The network denies everything by default and then allows only the paths the platform uses. Nodes are private, and Cloud SQL has only a private IP. Every database connection goes through the Cloud SQL Auth Proxy over mTLS. Secrets live in Secret Manager and reach pods as files through the GKE CSI driver, and a small preload script turns those files into environment variables. No Kubernetes Secret holds a credential.

Background work starts with a CronJob that calls the IAM app’s cron route once a minute, authorized with a shared secret. Each tick delivers Better IAM’s outbox (every invitation, password reset and sign-in code, sent through Mailjet) and dispatches its events and sweeps. A Postgres ledger runs each job once per tick, however many IAM pods there are. Next we’re moving the platform’s own billing, metering and signup jobs onto the same scheduler.

Share

Want the build notes as they happen?

The blog is written by the engineers shipping Nokuva, Tavoc, and Foltrac — deep dives land here as the work does, and the authors answer questions.