Features
What actually ships in Gravity, grouped the way the codebase is organized. See Architecture for how these fit together, and Backend & Adhara for what's Gravity's job versus what needs a backend.
Server & runtime
- FastAPI — router-based routing (
app/routes/*.py),Depends()-based dependency injection for auth (app/deps.py), Jinja2 server-side rendering, a factory-based app config (create_app()). Modern async-native Python, applied consistently — no Node.js, no build step. - Configurable server mode — plain Uvicorn for platforms that already autoscale
per-instance (Cloud Run, Container Apps), or Gunicorn supervising Uvicorn workers
for a standalone server/VM where nothing else respawns a crashed process. One env
var (
GRAVITY_SERVER_MODE) picks the mode; see Deployment. - JSON command API —
/api/messageroutes typed JSON commands (blog_new_post,contact_add,event_register, …) through a schema-validated dispatcher (app/messaging/) instead of one bespoke endpoint per action. - File upload — multipart upload through the same pluggable
ContentStoreevery other piece of content uses; lands in a local volume by default, a cloud bucket in production.
Deployment
- Four targets, one command —
make deploy PLATFORM=vercel|gcp|cloudflare|azure. Vercel needs no Docker (fastest first deploy); the other three build and push a container image. Every script auto-generates aSECRET_KEYand auto-provisions that platform's recommended storage backend when run non-interactively (by an agent), somake deployis genuinely one-shot. make push-repo— creates a GitHub or GitLab repository and pushes your code, detecting and safely routing around the case whereoriginstill points at Gravity's own shared template repo.VERCEL_LINK_GIT=1on top ofmake deploy PLATFORM=vercelconnects that repo in Vercel so future pushes trigger an automatic rebuild.- Docker + Compose — one
Dockerfile, onedocker-compose.ymlwiring together the app, port mapping, and persistent volume mounts for data/auth/uploads/certs. install.sh— a singlecurl | bashgets a local instance running natively, no Docker required, with status/stop/logs/update controls that never prompt — designed to be driven by an AI agent as easily as a human terminal.
Design system
- CSS design tokens — colors, fonts, spacing, radii, and shadows live in one
:root {}block per theme (public/themes/<theme>/main.css). Change the tokens, change the entire look. - Five swappable themes —
gravity(dark navy + gold, the default and fallback),clarity(Notion-inspired light),photo(editorial/photography),resonance(music-artist, audio-reactive hero),stripe(CSS + one page — the rest falls back togravity). Switch withGRAVITY_TEMPLATE; a theme missing a given template transparently falls back togravity's copy instead of 404ing. - Server-rendered, no build step — all HTML is Jinja2, rendered server-side. No hydration, no JS framework, no bundler. Fast first paint, full accessibility, and nothing to compile before a page reflects a change.
- Mobile responsive — every layout collapses cleanly; no per-page media-query work required.
Auth & admin
- JWT-backed admin auth, gated on the Adhara SDK in real deployments — see
Authentication for the full
require_admin/require_portalflow, including the localGRAVITY_DEV_ADMIN=1bypass for testing without an Adhara account. - Admin dashboard — manage blog posts, events, contacts, and users from one
interface; public and admin routes are cleanly separated
APIRouters. - Customer portal — a separate auth system (
/portal/*,require_portal) with silent session refresh, courses, community, resources, and certificates — Adhara-backed, with its own local dev bypass (GRAVITY_DEV_PORTAL=1).
The Visual Editor
- Block-based page content — pages are
{id, type, fields}sections validated againstschema/blocks/*.json, with a draft/published split so edits never go live until explicitly published. - Three ways to edit — inline click-to-edit, a full drag-and-configure Builder, or asking an AI assistant to edit the underlying JSON directly. See Visual Editor design for the full model.
- Theme Tweaks — the same draft/publish pattern applied to CSS custom-property overrides (colors, shadows, opacity) layered on top of a theme's stylesheet, rather than editing it — a build-time customization tool, gated separately from normal admin access.
Storage — the ContentStore abstraction
- Six backends, one interface — local JSON files, Google Cloud Storage, AWS S3,
Azure Blob, Cloudflare R2, and Vercel Blob all implement the same
read/write/describe/write_media/list/deletecontract (app/stores/base.py). Every deploy script can offer to provision the platform's own native option and default to it. - Local by default — with nothing configured, blog, events, contacts, the link-in-bio page, and even the customer portal all work immediately against local JSON. No database, no signup, no persistence guarantee — see Backend & Adhara for exactly where that stops being enough.
Adhara integration
- Two tiers, both optional — an API key unlocks blog, events, forms, and newsletter over Adhara's REST API; the Adhara SDK additionally unlocks admin login, Adhara Commerce checkout, and portal auth. Everything degrades gracefully when unconfigured instead of crashing.
- Configurable edge caching for Adhara-backed content (the blog, to start) —
a standard
Cache-Controlheader (app/caching.py) lets Vercel's and Cloudflare's edge networks serve a cached response instead of re-fetching from Adhara on every request, with the TTL and stale-while-revalidate window configurable viaGRAVITY_EDGE_CACHE_TTL/GRAVITY_EDGE_CACHE_SWR. Never applied to an authenticated admin's own view of the same page. See Deployment → Edge caching for what each platform actually needs to honor it. - Full picture, including what it costs and what building your own backend instead actually involves: Backend & Adhara.
SEO & branding
- Generated
sitemap.xmlandrobots.txt(app/controllers/seo.py) — static pages are discovered directly from the app's own router table (deduped by controller, so the///home//index//index.htmlaliases collapse to one entry), with published blog posts, listed events, and every docs page enumerated separately. A new page shows up with zero extra step the moment its route is registered. - Title/description/Open Graph/Twitter cards/canonical URL/favicon, computed
once per request (
app/theme.py::_global_context()) and available to every theme — a page that sets nothing still gets a complete, page-specific set (including a correctog:url/canonical per page, not the same URL repeated on every page). Override per page with the normal{% block title %}/{% block description %}/{% block og_image %}pattern. - One branding source of truth (
app/content.py::default_header(), editable live via the Visual Editor's Site Settings) — brand name, tagline, logo, and social-share image are all data, not strings baked into a template, so rebranding this framework for a different site doesn't mean editing five themes' HTML. - Full checklist for adding a new page (what's automatic, what to set by hand, how
to keep a page out of the sitemap):
AGENTS.mdat the repo root.