◉ANTIGRAVITY LABJP
Articles/Integrations
⬡ Integrations/2026-09-05Advanced

Pointing GOOGLE_GEMINI_BASE_URL at Your Own Gateway Does Not Change Who Holds the Key

I pointed the Antigravity CLI at a gateway of my own and recorded exactly which credentials travelled with it. Authorization is dropped across a cross-origin redirect, but the API key header survives every hop. Here is the measurement, and how I moved the key off my laptop.

antigravity458gemini19gatewayauthentication10integrations21

✦ Premium Article

I decided to put one layer between my machine and the model API. The motivation was modest: I wanted to count, in one place, how much each project was calling.

So I wrote my own URL into GOOGLE_GEMINI_BASE_URL and stood up a tiny server that printed whatever it received. It took a few minutes to work.

Then I read the headers that had arrived, and I stopped. The API key that lives on my laptop was sitting right there, one line among the rest.

In hindsight it could not have gone any other way. I changed the destination, and I never wrote an instruction to strip the credentials. Still, the fact that one line in a config file quietly changes where a key gets delivered felt lighter than I expected it to.

Antigravity CLI 1.1.13 added direct GEMINI_API_KEY authentication: set modelProvider: "gemini" in settings.json, export the variable, and the CLI runs without signing in. The same release note mentions that you can point GOOGLE_GEMINI_BASE_URL at a custom endpoint. It is the shortest path there is, so anyone aiming at a local engine or an internal gateway ends up here.

Which is exactly why it is worth looking, once, with your own eyes at what arrives on the other side. What follows was measured on my own machine, in the setup I use as an indie developer.

Stand up a receiver and record the headers as they arrive

Start with a recording, not a guess. This server does not forward anything upstream; it just collects headers.

# echo_srv.py - a receiver that records headers and nothing else
import threading, http.server, socketserver
 
LOGS = {}
 
def make_handler(name, redirect_to=None):
    LOGS[name] = []
 
    class H(http.server.BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
 
        def do_POST(self):
            n = int(self.headers.get("content-length", 0))
            self.rfile.read(n)                      # body discarded; headers are the subject
            LOGS[name].append({"path": self.path,
                               **{k.lower(): v for k, v in self.headers.items()}})
 
            # anything other than /final gets bounced once
            if redirect_to and not self.path.endswith("/final"):
                self.send_response(307)             # 307 preserves method and body
                self.send_header("Location", redirect_to)
                self.send_header("Content-Length", "0")
                self.end_headers()
                return
 
            body = b'{"ok":true}'
            self.send_response(200)
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
 
        def log_message(self, *a):                  # keep the default access log quiet
            pass
 
    return H
 
def serve(port, handler):
    socketserver.TCPServer.allow_reuse_address = True
    srv = socketserver.TCPServer(("127.0.0.1", port), handler)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    return srv

I picked 307 rather than 302 because some implementations rewrite a POST into a GET on a 302, which adds a variable I am not trying to measure. Only the thing under test should move.

With the destination swapped and nothing else, the credentials arrive intact

The plain path first, with no redirect in the way. Whatever the client sends shows up on the receiver.

import requests
 
HDRS = {
    "Authorization":        "Bearer ADC_TOKEN",   # dummy values; no real secrets here
    "x-goog-api-key":       "KEY",
    "x-goog-user-project":  "my-project",
    "Content-Type":         "application/json",
}
 
serve(8914, make_handler("plain"))
requests.post("http://127.0.0.1:8914/a", headers=HDRS, json={}, timeout=5)
 
d = LOGS["plain"][-1]
print(sorted(k for k in d if k in ("authorization", "x-goog-api-key")))
# output: ['authorization', 'x-goog-api-key']

Both arrive. A base_url setting chooses a route; it never touches how credentials are handled. Obvious once stated, but seeing it makes the next difference much easier to read.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You can record exactly which credentials leave your machine before you point a client at a gateway or a local engine, instead of finding out later
✦You can design your forwarded-header list around a real asymmetry: Authorization is stripped across origins while the API key header is not
✦You can move the upstream key off your laptop and onto a gateway in an order that never leaves you guessing whether the block actually works
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Integrations2026-05-24
Catching "Running but Doing Nothing" Antigravity Subagents — A 3-Layer Observability Pattern Across Six Production Apps
Running Antigravity subagents across six production apps surfaced more than ten silent failures every month — exits clean, logs green, but nothing actually happened. Here is the Heartbeat / Output Trace / Decision Log pattern I now use to catch them inside 60 seconds, with code, GCP costs, and four months of running numbers.
⬡ Integrations2026-05-04
Antigravity × Unreal Engine 5: In-Game AI Agents
A deep dive into integrating Antigravity into Unreal Engine 5 — three integration architectures, NPC implementation, procedural generation, performance budgets, and the three pitfalls I hit shipping a real project.
⬡ Integrations2026-04-30
Stateful AI Agents on Antigravity × Cloudflare Durable Objects — One Session, One Instance, One Place to Manage Conversation, Tools, and Cost
How to deploy an Antigravity AgentKit 2.0 agent onto Cloudflare Durable Objects so that conversation history, tool-call state, and token spend live inside a single instance per session — with the actual code and production cost numbers from my own deployment.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links