Web Apps

Serve HTML pages and static assets from a Rebase ASGI app — and recognise the cases where Rebase is the wrong host.

An ASGI app does not have to return JSON. Anything ASGI can serve HTML, CSS, images, and fonts, so a dashboard, an admin page, or a small internal tool is a normal Rebase deployment — no separate hosting account, and the app sits next to the secrets, volumes, and buckets it already needs.

This page covers what changes when the client is a browser rather than a script. Start with ASGI Apps for the deployment model itself.

A Page, Not an API

import rebase as rb

image = rb.Image.python("3.12").uv_pip_install("fastapi==0.141.1")


@rb.asgi_app(project="grid", name="grid-console", auth="public", image=image)
def grid_console():
    from fastapi import FastAPI
    from fastapi.responses import HTMLResponse

    web_app = FastAPI()

    @web_app.get("/", response_class=HTMLResponse)
    def index():
        return "<h1>Grid console</h1>"

    return web_app


rb.deploy(grid_console)

rebase deploy prints the URL. It has this shape:

/e/{workspace_id}/{environment}/{project_name}{base_path}

Static Assets

Ship a directory into the image with add_local_dir and mount it with Starlette's StaticFiles:

image = (
    rb.Image.python("3.12")
    .uv_pip_install("fastapi==0.141.1")
    .add_local_dir("./assets", "/srv/assets")
)


@rb.asgi_app(project="grid", name="grid-console", auth="public", image=image)
def grid_console():
    from fastapi import FastAPI
    from fastapi.staticfiles import StaticFiles

    web_app = FastAPI()
    web_app.mount("/assets", StaticFiles(directory="/srv/assets"), name="assets")
    return web_app

copy=False (the default) mounts the directory at runtime, so changing an asset redeploys without rebuilding the image. copy=True bakes it into an image layer instead — correct when a build step needs the files present, and slower to iterate on.

A few limits apply to everything the bundle carries: 25,000 files, 100 MB for any single file, and 250 MB expanded in total.

Some destinations are reserved and rejected at deploy time: /opt/rebase, /mnt/rebase, /app/.venv, /app/app, /app/python, and the usual /proc, /sys, /dev. Put assets somewhere of your own, such as /srv.

This is the one thing that reliably breaks a browser-facing app and not an API.

Rebase strips the route prefix before forwarding, so your app sees /assets/app.css while the browser is sitting at /e/{workspace_id}/{environment}/{project_name}/assets/app.css. An absolute link in your HTML resolves against the browser's origin, not your app's, and 404s.

Emit relative links, or tell FastAPI what its public prefix is:

@rb.asgi_app(
    project="grid",
    name="grid-console",
    auth="public",
    image=image,
    env={"PUBLIC_PREFIX": "/e/<workspace_id>/prod/grid"},
)
def grid_console():
    import os

    from fastapi import FastAPI

    web_app = FastAPI(root_path=os.environ["PUBLIC_PREFIX"])
    return web_app

root_path changes only the URLs FastAPI generates — routing still matches the stripped path the proxy delivers, which is exactly what arrives.

Do not build links from request.url_for(). The Host header is not forwarded, so the app sees its private Cloud Run hostname and url_for returns an absolute URL nobody outside the platform can reach. Use a path, not a URL.

Public Access

A browser cannot attach an API key to an image request, so a page meant for anonymous readers needs auth="public". Anything behind a login must authenticate callers itself — see the note in ASGI Apps on why the Authorization header is unavailable to your app, and carry your own session cookie or custom header instead.

Cold Starts

An app with no traffic scales to zero, and the next visitor pays the cold start. For an unbuilt image that includes a uv pip install on boot, which a person waiting on a page will notice. Two ways to avoid it:

@rb.asgi_app(project="grid", name="grid-console", min_instances=1, image=image)

min_instances=1 keeps an instance warm at the cost of running it continuously. Warm instances are a per-workspace grant set by Rebase (default 0), so a deploy with min_instances above the grant fails with 409. Giving the app a built image — any add_local_* or uv_sync operation turns the build on — moves dependency installation to deploy time, which shortens the cold start whether or not you keep an instance warm.

What Rebase Does Not Host

The runtime is Python and the transport is a buffering HTTP proxy. That rules out a few things outright:

Not supportedWhy
Node, Next.js, Deno, static-site serversrb.Image builds Python images only — there is no custom base image or Dockerfile
WebSocketsThe proxy speaks HTTP only
Streaming responses, SSE, long pollingThe proxy reads the full upstream response before replying
Custom domainsApps are served under the API host's /e/... prefix
Response compression at the edgeThe proxy does not compress what it forwards

A JavaScript app can still be served here as long as something else builds it: run the build in CI, commit or bundle the output, and hand the directory to add_local_dir. What cannot move here is a framework that needs a Node server at request time.

When to Host Elsewhere

Rebase is the right home for a page that talks to your Rebase data — the app runs beside your secrets and buckets, and gets deployment, auth, and metering for free.

It is the wrong home for a public, asset-heavy, latency-sensitive site. Every request, including every image and stylesheet, crosses the platform API before it reaches your app. Both run in europe-west3 (Frankfurt), but each request still carries a route lookup, an auth check, and a usage charge. A CDN-backed host will beat that comfortably for a documentation site or a marketing page, and the gap is structural rather than something a bigger instance fixes.

  • ASGI Apps — the deployment model, auth, and dependencies
  • Endpoints — one function, one route, with run records
  • Buckets and Volumes — storage an app can mount

On this page