# HydroGo — Complete Technical Documentation > Official comprehensive technical context for AI assistants, LLMs, and software agents. > Source: https://hydrogo.dev > Updated: 2026-11-15 ================================================================================ # HYDROGO FRAMEWORK OVERVIEW & ARCHITECTURE ================================================================================ HydroGo is an enterprise-grade, single-binary headless CMS, commerce backend, and application engine written in Go. - **Repository:** https://github.com/hydrohq/hydrogo - **License:** PolyForm Shield 1.0.0 - **Admin Panel:** Embedded Svelte 5 single-page application served directly from the Go binary. - **Performance:** 42,000+ requests/sec with 1.2ms average latency. - **Autonomous AI Layer:** Built-in Model Context Protocol (MCP) server for Claude, Cursor, and AI agent integration. - **Stewardship:** Built and maintained by Arda Labs (https://ardalabs.dev). ================================================================================ # SECTION: INTRODUCTION ================================================================================ --- # What is HydroGo *Section: Introduction* *A single-binary headless content management system written in Go. This page explains what it does, what it deliberately does not do, and who it is for.* HydroGo manages **pages, news, media, members** and your own **content types**, and exposes all of it over a unified REST API. It ships as one self-contained executable that also embeds an admin panel, so a complete content backend is one download, one config file and one data directory. It is headless: HydroGo does not render your public website. Go serves JSON; you build the frontend with whatever you like (SvelteKit, Next.js, Astro, a mobile app, a static site generator) and read content from the API. ## What you get - **Pages and news.** Pages are made of blocks; news has categories, tags, scheduled publishing and revisions. - **Media library.** Uploads are converted to WebP, get focal-point crops and BlurHash placeholders, and can live on disk or S3. - **Content types.** Declare FAQs, team members, events or products in one YAML file and get admin screens and a public API without writing Go or Svelte. - **Members.** Optional sign-up and sign-in for your visitors, with a token API for mobile apps. - **Admin panel.** An embedded Svelte 5 single-page app with roles, revisions, an audit trail, MFA and an English and Turkish interface. - **Operations.** Migrations with rollback snapshots, backups and restore, health and readiness probes, Prometheus metrics, optional Redis and S3. ## What it deliberately is not | HydroGo does not… | Instead | | --- | --- | | Render a public website | Build a frontend on the API. See [Frontends](https://hydrogo.dev/docs#frontends/client) and the SvelteKit starter. | | Generate HTML on the server (except transactional e-mail) | Go serves JSON. Admin markup lives in the embedded Svelte app. | | Need a rebuild when content changes | Publishing is live on the next API request. See [Zero build on publish](https://hydrogo.dev/docs#introduction/architecture#zero-build-on-publish). | | Act as a framework or product factory | It stays a CMS: content, media, members and the API around them. | ## Is it a fit? HydroGo suits marketing sites, blogs, documentation sites, news portals and mobile app backends where editors need a friendly admin and developers want a small, predictable API and a simple deployment. > [!TIP] If your need is "a few structured collections plus pages and media", start with a preset and a couple of YAML files. Reach for custom Go modules only when behaviour, not just data, is different. ## Where to go next - **[Install](https://hydrogo.dev/docs#getting-started/install)**: Windows installer, install script, Docker or build from source. - **[Quick start](https://hydrogo.dev/docs#getting-started/quick-start)**: Scaffold a project and see the admin and API in a minute. - **[Content types](https://hydrogo.dev/docs#content-types/overview)**: Model your own data in YAML. - **[REST API](https://hydrogo.dev/docs#rest-api/overview)**: Envelope, auth, endpoints and curl examples. --- # Architecture *Section: Introduction* *How the binary, the admin panel, the API and the storage layers fit together, and why publishing never needs a rebuild.* *Architecture Diagram: architecture (Single-binary layout: Embedded SQLite + Memory Buffer + Gin Router + Admin SPA)* ## One binary, three surfaces `hydro serve` starts a single HTTP server that exposes three things: - `/api/v1` the REST API your frontends call. JSON only, every response uses the same envelope. - `/admin` the embedded admin panel (a Svelte 5 SPA compiled into the binary). - `/media`, `/sitemap.xml`, `/healthz`, `/readyz` and `/metrics` for media delivery, SEO helpers and operational probes. ## Modules and shared core Content and commerce features live in modules under `modules/` (`pages`, `news`, `members`, `site`, `seo`, `commerce`). Cross-cutting capabilities live in `internal/` (auth, database, media, search, security, jobs, webhooks, caching). Which modules run is configured with the `modules` list in `config.yaml`; an inactive module adds no routes, migrations, workers or admin navigation entries. | Layer | Responsibility | | --- | --- | | Transport (`transport/http.go`) | Decode requests, call a service, write the JSON envelope. No SQL, no HTML. | | Service | Business rules: validation, publishing, revisions, permissions. | | Repository (`repository/sqlstore`) | All SQL and dialect differences for SQLite, PostgreSQL and MySQL. | | Content types (YAML) | Declarative collections: storage, validation, admin screens and public API are derived from the schema. | | Commerce module (`modules/commerce`) | Cart, stock reservation, checkout, orders, payment gateways, and shipping/invoicing. | ## Core vs HydroCloud overlay HydroGo follows an open-core architecture: - **HydroGo Core (`hydrohq/hydrogo`):** The standalone, PolyForm Shield licensed engine containing the complete CMS, embedded Svelte 5 admin studio, YAML content engine, and commerce core. - **HydroCloud (`hydrohq/hydrocloud`):** A private multi-tenant cloud overlay applied via build tags (`//go:build hydrocloud`). It adds automated tenant orchestration, DNS automation, billing/credits, and multi-tenant admin consoles without duplicating core logic. ## Zero build on publish *Architecture Diagram: publish-flow (Single-binary layout: Embedded SQLite + Memory Buffer + Gin Router + Admin SPA)* The public API reads straight from the database. When an editor publishes, retracts or edits a page, a news article or a content record, the next request already returns the new state. This is a hard rule of the project: **content changes never require a frontend rebuild, a static pre-rendering pipeline or a CI/CD redeploy.** For frontends this means server-side rendering (or client-side fetching) against the API, optionally behind a short cache you control. If you do choose a static frontend, use [webhooks](https://hydrogo.dev/docs#frontends/caching-and-webhooks) to trigger a rebuild; that is your choice, not a requirement of HydroGo. ## Databases and optional services | Component | Default | Alternatives | | --- | --- | --- | | Database | SQLite file (`data/hydro.db`) | PostgreSQL, MySQL / MariaDB | | Media storage | Local directory (`data/media`) | S3-compatible bucket, optional CDN base URL | | Rate limits and caches | In process | Redis or Valkey, shared by several instances | | Errors | Logs | Sentry / GlitchTip via `observability.sentry_dsn` | > [!NOTE] Design decisions are recorded as short ADRs in the repository's `docs/decisions/` folder: CMS focus, content types, the Svelte admin SPA and the template distribution model. --- # Core concepts *Section: Introduction* *The vocabulary used throughout the docs: modules, content types, locales, roles, drafts and revisions.* ## Content | Concept | Meaning | | --- | --- | | Page | A document with a slug, a locale, a body and blocks. Has draft and published states and revisions. | | News article | Like a page, plus categories, tags, scheduled publishing and retraction. | | Content type | A collection you define in YAML, for example `faq` or `team`. Records get admin screens and, if `public: true`, a read API. | | Media | Uploaded files with derivatives (WebP, thumbnails), alt text and focal points. | | Member | A visitor account of your site, separate from admin users. | | Site settings | Business profile, navigation, redirects, forms, maintenance mode and translations. | ## Publishing states Pages and news have a draft and a published state. Content types opt in with `drafts: true`; new records then start as drafts and the public API never returns drafts. With `revisions: true` HydroGo keeps the last 50 versions of each record so editors can compare and restore. ## Locales HydroGo supports several content locales per site (`i18n` config). Pages and news are per-locale documents; content type fields marked `localized: true` hold a value per locale and fall back to the default locale. This is separate from the **admin interface language**. See [Multi-language](https://hydrogo.dev/docs#multi-language/content-locales). ## People | Role | Can | | --- | --- | | `admin` | Everything, including users, roles, settings and system tools. | | `editor` | Create and edit content within granted permissions. | | `viewer` | Read-only access to the admin. | Fine-grained permissions can be granted per user on top of the role. Members (site visitors) never see the admin. Read more in [Security](https://hydrogo.dev/docs#security/accounts-and-access). ## Credentials Three kinds of credential work against admin endpoints through the same middleware: a **session cookie** with a CSRF header (the admin SPA), a **Bearer token**, or an **API key**. See [Authentication](https://hydrogo.dev/docs#rest-api/authentication). > [!WARNING] Keep brand names, menus, dates and demo credentials out of the core codebase; they belong in seed data, config or your own frontend. This is a rule contributors follow, and a good habit in your own projects too. ================================================================================ # SECTION: GETTING STARTED ================================================================================ --- # Install *Section: Getting started* *Get the hydro command on Windows, Linux, macOS, in Docker, or build it from source.* Every method gives you one executable named `hydro`. Releases are published on GitHub for Linux, macOS and Windows (amd64 and arm64) together with a `SHA256SUMS` file; the install scripts verify the checksum for you. ## Choose a method | Platform | How | | --- | --- | | Windows (installer) | Download and run `HydroGo-Setup--windows-amd64.exe` (or `arm64`). It installs `hydro` and adds it to PATH. Open a new terminal afterwards. | | Windows (PowerShell) | `irm .../install.ps1 | iex`, see below. | | Linux and macOS | `curl -fsSL .../install.sh | sh`, see below. | | Docker | `ghcr.io/hydrohq/hydrogo` for linux/amd64 and arm64. | | From source | `go build -o bin/hydro ./cmd/hydro` with Go 1.27.2 or newer. | ## Windows Download the Setup executable from the [latest release](https://github.com/hydrohq/hydrogo/releases/latest) and run it. Leave **Add hydro to PATH** ticked. No administrator rights are needed: it installs per user, and you can choose to install for all users to use Program Files instead. Without the installer, use PowerShell: ```powershell irm https://github.com/hydrohq/hydrogo/releases/latest/download/install.ps1 | iex ``` ## Linux and macOS ```bash curl -fsSL https://github.com/hydrohq/hydrogo/releases/latest/download/install.sh | sh ``` Pin a specific version with the `HYDRO_VERSION` environment variable: ```bash HYDRO_VERSION=v0.2.0 sh install.sh ``` ## Docker ```bash docker run -p 8080:8080 -v hydro-data:/app/data ghcr.io/hydrohq/hydrogo:latest ``` Mount your own `config.yaml` at `/app/config.yaml` and keep the data directory on a volume. A complete Compose file with Caddy is described in [Docker deployment](https://hydrogo.dev/docs#deployment/docker). ## From source You need Go 1.27.2 or newer (the toolchain is pinned in `go.mod`). The admin panel is committed pre-built, so Node.js is not required to build the binary. ```bash git clone https://github.com/hydrohq/hydrogo.git cd hydrogo go build -o bin/hydro ./cmd/hydro ``` ## Verify the install ```bash hydro version hydro version --json hydro help ``` > [!TIP] `hydro help` prints every command. The full list is also on the [CLI reference](https://hydrogo.dev/docs#reference/cli) page. --- # Quick start *Section: Getting started* *From nothing to a running admin panel and API in about a minute.* ## 1. Create a project ```bash hydro new mysite --preset blog --admin-email you@example.com cd mysite ``` `hydro new` creates the folder and writes a `config.yaml` and the preset's `content-types/`. It then creates the SQLite database and the first admin user. If you do not pass `--admin-password-stdin` (or set `HYDRO_ADMIN_PASSWORD`), a strong password is generated and **printed once**, so copy it. > [!NOTE] Without `--admin-email`, `hydro new` only writes files and prints the next commands. Use that when you want to create the admin later with `hydro create-admin`. Provide the password from a pipe instead of generating one: ```bash printf '%s' "$ADMIN_PASSWORD" | hydro new mysite --preset blog \ --admin-email you@example.com --admin-password-stdin ``` ## 2. Start the server ```bash hydro serve --config config.yaml ``` | URL | What | | --- | --- | | `http://127.0.0.1:8080/admin` | Admin panel. Sign in with the e-mail and password from step 1. | | `http://127.0.0.1:8080/api/v1` | REST API. | | `http://127.0.0.1:8080/healthz` | Liveness probe. | ## 3. Create content and read it back In the admin, create and publish a news article (or a page), then read it through the API: ```bash curl 'http://127.0.0.1:8080/api/v1/news?locale=en&limit=5' ``` ```json { "data": [ { "id": "…", "locale": "en", "slug": "hello-world", "title": "Hello world", "status": "published" } ], "metadata": { "limit": 5, "has_more": false } } ``` ## 4. Add your own content type ```bash hydro content-type new recipes # edit content-types/recipes.yaml, then: hydro content-type validate hydro serve --config config.yaml ``` The admin now has screens at `/admin/content/recipes` and, with `public: true`, the API serves `/api/v1/content/recipes`. See [Content types](https://hydrogo.dev/docs#content-types/overview). ## 5. Build your site Use the API from any frontend. Two starting points are the typed [`@hydrogo/client`](https://hydrogo.dev/docs#frontends/client) and the server-rendered [SvelteKit starter](https://hydrogo.dev/docs#frontends/sveltekit-starter). ## Use it in the current directory `hydro init` does the same as `hydro new` but scaffolds into the current directory and accepts the same options. ```bash mkdir mysite && cd mysite hydro init --preset corporate --admin-email you@example.com ``` > [!WARNING] Run `hydro new` and `hydro init` on a fresh folder. They write `config.yaml` and `content-types/` and are meant for a new project, not for overwriting one that is already in use. --- # Presets *Section: Getting started* *The five starting points for hydro new, which modules they enable and which content types they copy into your project.* A preset is a starting point: a list of enabled modules and a set of stock content types copied into your project's `content-types/` folder. Everything is plain files afterwards, so you can delete types you do not need and add your own. ```bash hydro new --list-presets ``` | Preset | For | Content types copied | | --- | --- | --- | | `minimal` | Pages and media only | `site_settings`, `site_navigation` | | `blog` | News or blog posts, pages, comments, galleries | + `comments`, `galleries`, `faq` | | `corporate` | Company site | + `team`, `testimonials`, `faq` | | `news` | News portal | + `homepage_sections`, `masthead`, `columnists`, `official_announcements`, `ads`, `polls`, `videos`, `galleries`, `comments` | | `docs` | Documentation site | + `faq` | ## Enabled modules per preset | Preset | Modules | | --- | --- | | `minimal` | core, auth, pages, media, site, collections | | `blog` | core, auth, members, pages, media, site, news, collections, analytics | | `corporate` | core, auth, pages, media, site, collections | | `news` | core, auth, members, pages, media, site, news, collections, analytics | | `docs` | core, auth, pages, media, site, collections | ## Changing your mind later - To add a stock type, copy its YAML from the repository's `content-types/` folder into your project and restart. - To turn a module on or off, edit the `modules` list in `config.yaml` and run `hydro migrate`. An inactive module adds no routes, migrations or admin navigation. - Delete any content type file you do not need. Run `hydro content-type validate` afterwards. > [!NOTE] Stock content types are examples. Fields, labels and options are meant to be edited to fit your project. --- # Project layout *Section: Getting started* *What hydro new writes to disk, what is safe to commit, and where each kind of change belongs.* ## A scaffolded project ```text mysite/ ├── config.yaml # runtime configuration (see Configuration) ├── content-types/ # one YAML file per content type │ ├── site_settings.yaml │ └── faq.yaml ├── data/ # created at first start: SQLite database and media │ ├── hydro.db │ └── media/ ├── .gitignore └── README.md ``` | Path | Commit it? | Notes | | --- | --- | --- | | `config.yaml` | Yes, without secrets | Put DSNs, keys and passwords in `HYDRO_*` environment variables or a protected secret file. | | `content-types/` | Yes | Your data model. Review changes like code. | | `data/` | No | Database and uploaded media. Back it up instead. | ## Layout of the HydroGo repository Useful if you build from source or contribute. | Path | What lives there | | --- | --- | | `cmd/hydro` | The CLI and HTTP server entrypoint. | | `admin/` | Svelte 5 + TypeScript + Tailwind admin panel (embedded into the binary). | | `internal/` | Core packages: auth, database, media, collections, search, security, scaffolding. | | `modules/` | Content modules: `pages`, `news`, `members`, `site`, `seo`. | | `content-types/` | Stock YAML content types. | | `migrations/` | SQL migrations for SQLite, PostgreSQL and MySQL. | | `packages/client` | `@hydrogo/client`, the typed API client. | | `examples/` | Frontend starters, including this site. | | `deploy/`, `scripts/` | Docker, Caddy, nginx, hosting examples, install scripts, Windows installer. | | `docs/` | Guides, API baselines, JSON Schema and decision records. | ## Where a change goes | You are adding… | Put it in | | --- | --- | | A simple content type (FAQ, team, events) | One YAML file in `content-types/`. No Go, no Svelte. | | Content module logic | `modules//` with `model.go`, a service, `repository/sqlstore/` and `transport/http.go`. | | A shared capability | `internal//`. | | An admin screen | `admin/src/routes//`. | | A database change | A new directory in `migrations/` with SQL for all three dialects. | | A CLI command | Its own file in `cmd/hydro/_cmd.go`. | > [!TIP] Use `hydro dev` (alias of `hydro run`) or the repository's `go run ./tools/devserver` while working on HydroGo itself. It rebuilds on save and seeds disposable demo data. ================================================================================ # SECTION: CONFIGURATION ================================================================================ --- # config.yaml reference *Section: Configuration* *Every commonly used key in config.yaml, what it does and its default. Copy config.example.yaml from the repository for a commented starting point.* HydroGo reads `config.yaml` (path given with `--config`). Every key can also be overridden with a `HYDRO_*` environment variable, listed on [Environment variables](https://hydrogo.dev/docs#configuration/environment-variables). Environment variables win over the file, and command-line runtime flags win over both. ```yaml // config.yaml profile: compact modules: [core, auth, members, pages, media, site, news, collections] http: address: "127.0.0.1:8080" public_origin: "https://cms.example.com" database: dialect: "sqlite" dsn: "data/hydro.db" media: dir: "data/media" min_free_bytes: 52428800 i18n: enabled: true default_locale: en locales: [en, tr] security: secure_cookies: true enable_hsts: true audit: log_request_bodies: false ``` > [!TIP] Validate a file before you start the server: `hydro validate --config config.yaml`. It reports configuration and database problems with a remediation hint. ## Top level | Key | Default | Meaning | | --- | --- | --- | | `profile` | `compact` | Capacity profile: `compact`, `standard` or `distributed`. See [Modules and profiles](https://hydrogo.dev/docs#configuration/modules-and-profiles). | | `modules` | per preset | Which modules run: `core`, `auth`, `members`, `pages`, `media`, `site`, `news`, `collections`, `analytics`. | | `content_types_dir` | `content-types` | Folder with the content type YAML files. | | `safe_mode` | `false` | Start with a restricted feature set for recovery. | ## http | Key | Default | Meaning | | --- | --- | --- | | `address` | `127.0.0.1:8080` | Listen address. Use `0.0.0.0:8080` in containers. | | `public_origin` | empty | Canonical public HTTPS origin of the CMS, for example `https://cms.example.com`. Set it in every deployed environment. | | `behind_proxy` | `false` | Trust that a reverse proxy terminates TLS in front of HydroGo. | | `auto_tls` | `false` | Obtain certificates automatically. Needs `domain` and cannot be combined with `behind_proxy`. | | `domain` | empty | The single domain used for automatic TLS. | ## database | Key | Default | Meaning | | --- | --- | --- | | `dialect` | `sqlite` | `sqlite`, `postgres` or `mysql` (MySQL and MariaDB). | | `dsn` | empty | Connection string. Prefer `HYDRO_DATABASE_DSN` or a protected secret file over putting credentials in the YAML. | | `sqlite_external_write_watch` | `false` | Detect external writers to the SQLite file. | ```text // DSN examples # PostgreSQL postgres://hydro_app:REPLACE_ME@db.internal:5432/hydro?sslmode=verify-full # MySQL / MariaDB hydro:REPLACE_ME@tcp(db.internal:3306)/hydro # SQLite (a file path) data/hydro.db ``` ## media | Key | Default | Meaning | | --- | --- | --- | | `dir` | `data/media` | Local media directory. | | `min_free_bytes` | `52428800` | Refuse uploads when free disk space drops below this (50 MiB). | | `storage_driver` | local | `s3` stores media in an S3-compatible bucket. | | `s3_endpoint`, `s3_bucket`, `s3_region` | empty | S3 location. | | `s3_access_key`, `s3_secret_key` | empty | S3 credentials. Use environment variables. | | `s3_use_ssl`, `s3_force_path_style` | | S3 connection options (path-style is needed by many self-hosted stores). | | `cdn_base_url` | empty | Serve media URLs from a CDN origin. | | `hotlink_protection`, `allowed_referer_hosts` | off | Block other sites from embedding your media. | | `watermark_text`, `watermark_position`, `watermark_opacity`, `watermark_margin` | off | Optional text watermark on image derivatives. | ## i18n | Key | Default | Meaning | | --- | --- | --- | | `enabled` | | Turn on content locales. | | `default_locale` | `en` | Fallback locale. Cannot be empty. | | `locales` | | List of 1 to 64 locale codes such as `en`, `tr`, `pt-BR`. | Content locales are covered in [Content locales](https://hydrogo.dev/docs#multi-language/content-locales). ## security | Key | Default | Meaning | | --- | --- | --- | | `secure_cookies` | `false` | Mark cookies `Secure`. Keep `false` for plain-HTTP local development, `true` on HTTPS. | | `enable_hsts` | `false` | Send `Strict-Transport-Security`. Enable only when HTTPS works everywhere. | | `headers_preset` | `strict` | Preset of security response headers. | | `enable_anomaly_filter` | `true` | Block obviously abusive requests. | | `anomaly_bypass_paths` | `[]` | Paths exempt from the anomaly filter. | | `mfa_required_roles` | `[]` | Roles that must use MFA, for example `[admin]`. Enrol the accounts first. | | `mfa_master_key` | empty | Secret (16, 24 or 32 bytes) that protects stored TOTP seeds. | | `public_api_origins` | `[]` | Explicit allowlist of browser origins that may read published `/api/v1` content without credentials. Wildcards are not supported. | | `preview_signing_key` | empty | At least 32 bytes; signs draft preview links. | | `password_reset_ttl`, `invitation_ttl` | built-in | Lifetime of reset and invitation links, for example `1h`. | ## audit | Key | Default | Meaning | | --- | --- | --- | | `log_request_bodies` | `false` | Record the redacted, size-capped JSON body of every state-changing admin API request in the audit trail. See [Audit trail](https://hydrogo.dev/docs#security/audit-trail). | ## redis | Key | Default | Meaning | | --- | --- | --- | | `url` | empty | Redis or Valkey URL, e.g. `rediss://:secret@host:6379/0`. Leave empty for single-instance setups. | | `key_prefix` | `hydro:` | Prefix for all keys, so several apps can share one Redis. | ## mail and forms | Key | Meaning | | --- | --- | | `mail.host`, `mail.port`, `mail.username`, `mail.password`, `mail.from`, `mail.starttls` | SMTP server for transactional e-mail (invitations, password resets, notifications). | | `mail.dkim_selector` | DKIM selector for signed mail. | | `mail.max_per_second`, `mail.max_per_hour` | Outgoing mail rate limits. | | `forms.turnstile_site_key`, `forms.turnstile_secret_key` | Cloudflare Turnstile keys for public forms. | ## seo, analytics, geoip, observability | Key | Meaning | | --- | --- | | `seo.og_cache_dir`, `seo.fonts_dir` | Cache and font folders for generated Open Graph images (`/og/{locale}/{slug}`). | | `analytics.no_cookie_enabled`, `analytics.hash_key` | Opt-in cookie-free daily visitor summaries; the hash key must be 32+ random bytes. | | `geoip.database_path`, `geoip.trusted_proxy_cidrs` | Optional offline GeoIP database and the proxy ranges that may supply location headers. | | `observability.sentry_dsn` | Sentry or GlitchTip DSN for error reporting. | ## webhooks and backup `webhooks` is a list of outgoing endpoints; see [Caching and webhooks](https://hydrogo.dev/docs#frontends/caching-and-webhooks). Scheduled cloud backups use the `backup` block. ```yaml webhooks: - id: frontend-rebuild url: https://ci.example.com/hooks/rebuild topics: [page.published, news.published, news.retracted] backup: enabled: false prefix: backups/ retention_days: 30 hour_utc: 3 ``` > [!NOTE] `backup.enabled: true` requires configured S3-compatible credentials, a `retention_days` between 1 and 3650 and an `hour_utc` between 0 and 23. Otherwise the config fails validation. ## Other blocks - `tenancy` (`enabled`, `require_host`, `hosts`) maps hostnames to tenants for multi-tenant deployments. - `admin_support` shows an optional support contact card inside the admin (`agency_name`, `email`, `phone`, `whatsapp_url`, `sla_badge`, `guide_url`). - `admin_extensions` (`manifest_dir`, `trusted_keys_file`) loads signed, externally hosted admin surfaces. - `license` (`required`, `file`, `project_id`, `edition`, `features`) is only relevant to licensed editions. --- # Environment variables *Section: Configuration* *Every HYDRO_* variable that overrides config.yaml, grouped by area. Use them for secrets and per-environment values.* Environment variables override the matching `config.yaml` key. Boolean values accept `true` and `false`; lists are comma separated. Invalid values stop the start-up with a message naming the variable. > [!WARNING] Keep secrets out of `config.yaml` and out of version control. Pass them as environment variables, through your orchestrator's secret store, or from a protected file. ## Core | Variable | Overrides | | --- | --- | | `HYDRO_HTTP_ADDRESS` | `http.address` | | `HYDRO_PUBLIC_ORIGIN` | `http.public_origin` | | `HYDRO_CAPACITY_PROFILE` | `profile` | | `HYDRO_SAFE_MODE` | `safe_mode` | | `HYDRO_CONTENT_TYPES_DIR` | `content_types_dir` | | `HYDRO_DATABASE_DIALECT` | `database.dialect` | | `HYDRO_DATABASE_DSN` | `database.dsn` | | `HYDRO_SQLITE_EXTERNAL_WRITE_WATCH` | `database.sqlite_external_write_watch` | | `HYDRO_REDIS_URL`, `HYDRO_REDIS_KEY_PREFIX` | `redis.url`, `redis.key_prefix` | | `HYDRO_ADMIN_PASSWORD` | Password for the admin user created by `hydro new` or `hydro init`. | ## Security | Variable | Overrides | | --- | --- | | `HYDRO_SECURE_COOKIES` | `security.secure_cookies` | | `HYDRO_ENABLE_HSTS` | `security.enable_hsts` | | `HYDRO_PUBLIC_API_ORIGINS` | `security.public_api_origins` | | `HYDRO_MFA_MASTER_KEY` | `security.mfa_master_key` | | `HYDRO_PREVIEW_SIGNING_KEY` | `security.preview_signing_key` | | `HYDRO_PASSWORD_RESET_TTL`, `HYDRO_INVITATION_TTL` | `security.password_reset_ttl`, `security.invitation_ttl` | | `HYDRO_AUDIT_LOG_REQUEST_BODIES` | `audit.log_request_bodies` | ## Media | Variable | Overrides | | --- | --- | | `HYDRO_MEDIA_DIR`, `HYDRO_MEDIA_MIN_FREE_BYTES` | `media.dir`, `media.min_free_bytes` | | `HYDRO_MEDIA_STORAGE_DRIVER` | `media.storage_driver` (`s3` for object storage) | | `HYDRO_MEDIA_S3_ENDPOINT`, `_BUCKET`, `_REGION`, `_ACCESS_KEY`, `_SECRET_KEY` | The `media.s3_*` keys (prefix each with `HYDRO_MEDIA_S3`) | | `HYDRO_MEDIA_S3_USE_SSL`, `HYDRO_MEDIA_S3_FORCE_PATH_STYLE` | `media.s3_use_ssl`, `media.s3_force_path_style` | | `HYDRO_MEDIA_CDN_BASE_URL` | `media.cdn_base_url` | | `HYDRO_MEDIA_HOTLINK_PROTECTION`, `HYDRO_MEDIA_ALLOWED_REFERER_HOSTS` | Hotlink protection and its allowed hosts | | `HYDRO_MEDIA_WATERMARK_TEXT`, `_POSITION`, `_OPACITY`, `_MARGIN` | Watermark settings (prefix `HYDRO_MEDIA_WATERMARK`) | ## Languages | Variable | Overrides | | --- | --- | | `HYDRO_I18N_ENABLED` | `i18n.enabled` | | `HYDRO_DEFAULT_LOCALE` | `i18n.default_locale` | | `HYDRO_LOCALES` | `i18n.locales`, comma separated, for example `en,tr` | ## Mail, forms and integrations | Variable | Overrides | | --- | --- | | `HYDRO_SMTP_HOST`, `_PORT`, `_USERNAME`, `_PASSWORD`, `_FROM`, `_STARTTLS`, `_DKIM_SELECTOR` | The `mail.*` keys (prefix `HYDRO_SMTP`) | | `HYDRO_SMTP_MAX_PER_SECOND`, `HYDRO_SMTP_MAX_PER_HOUR` | `mail.max_per_second`, `mail.max_per_hour` | | `HYDRO_FORMS_TURNSTILE_SITE_KEY`, `HYDRO_FORMS_TURNSTILE_SECRET_KEY` | `forms.turnstile_*` | | `HYDRO_WEBHOOK_URL`, `HYDRO_WEBHOOK_SECRET`, `HYDRO_WEBHOOK_TOPICS` | A single webhook endpoint without editing YAML | | `HYDRO_SEO_OG_CACHE_DIR`, `HYDRO_SEO_FONTS_DIR` | `seo.og_cache_dir`, `seo.fonts_dir` | | `HYDRO_ANALYTICS_NO_COOKIE_ENABLED`, `HYDRO_ANALYTICS_HASH_KEY` | `analytics.*` | | `HYDRO_SENTRY_DSN` | `observability.sentry_dsn` | | `HYDRO_BACKUP_SIGNING_SEED` | Hex seed (64 characters) that signs backup manifests | ## Example: production environment file ```env // .env HYDRO_CAPACITY_PROFILE=standard HYDRO_HTTP_ADDRESS=0.0.0.0:8080 HYDRO_PUBLIC_ORIGIN=https://cms.example.com HYDRO_SECURE_COOKIES=true HYDRO_ENABLE_HSTS=true HYDRO_DATABASE_DIALECT=postgres HYDRO_DATABASE_DSN=postgres://hydro_app:REPLACE_ME@db.internal:5432/hydro?sslmode=verify-full HYDRO_REDIS_URL=rediss://:REPLACE_ME@redis.internal:6379/0 HYDRO_MFA_MASTER_KEY=REPLACE_WITH_16_24_OR_32_BYTE_SECRET HYDRO_MEDIA_STORAGE_DRIVER=s3 HYDRO_MEDIA_S3_ENDPOINT=s3.eu-central-1.amazonaws.com HYDRO_MEDIA_S3_BUCKET=example-media HYDRO_MEDIA_S3_REGION=eu-central-1 HYDRO_MEDIA_CDN_BASE_URL=https://cdn.example.com HYDRO_SMTP_HOST=smtp.example.com HYDRO_SMTP_PORT=587 HYDRO_SMTP_FROM=noreply@example.com ``` > [!TIP] The repository ships `deploy/env.production.example`, `env.staging.example` and `env.dev.example` as templates. --- # Modules and profiles *Section: Configuration* *Turn features on and off with the modules list, and choose a capacity profile that matches your deployment size.* ## Modules A module is a unit of functionality with its own routes, migrations, workers and admin navigation. An inactive module adds none of those. Enable what you need in `modules`, then run `hydro migrate`. | Module | Provides | | --- | --- | | `core` | HTTP server, config, health endpoints. Always on. | | `auth` | Admin users, sessions, roles, MFA, API keys and tokens. | | `members` | Visitor accounts and the member token API. | | `pages` | Pages with blocks, revisions and preview links. | | `media` | Media library and delivery. | | `site` | Site settings, navigation, redirects, forms, sitemap and maintenance mode. | | `news` | News articles, categories, tags and scheduled publishing. | | `collections` | The engine behind YAML content types. | | `analytics` | Opt-in, privacy-friendly traffic summaries. | ## Capacity profiles A profile sets sensible defaults for how much machinery runs inside the process. Providers such as the database, S3 and Redis are chosen separately; the profile does not infer them. | Profile | Use for | Adds | | --- | --- | --- | | `compact` | Single small site, local development, shared hosting | In-memory caches, minimal background work. | | `standard` | A production site on a VPS or container | Bounded background workers, optional distributed cache. | | `distributed` | Several instances behind a load balancer | Everything in `standard`, plus tenant admission and OpenTelemetry export (opt-in). | ```bash hydro serve --config config.yaml --capacity-profile standard # or HYDRO_CAPACITY_PROFILE=standard hydro serve --config config.yaml ``` > [!NOTE] Running more than one instance? Use PostgreSQL or MySQL and set `redis.url`, so rate limits and caches are shared. ================================================================================ # SECTION: CONTENT TYPES ================================================================================ --- # Content types *Section: Content types* *Model anything beyond pages and news with one YAML file. HydroGo derives storage, validation, admin screens and a public API from it.* Anything that is not a page, a news article or a member is a **content type**: one YAML file in `content-types/`. No Go and no Svelte are involved. From the file HydroGo derives: - database storage and validation, - admin screens at `/admin/content/` (list, search, filters, edit form, drafts, revisions), - with `public: true`, a read API at `/api/v1/content/` (see [Content API](https://hydrogo.dev/docs#rest-api/content-api)). ## A complete example ```yaml // content-types/recipes.yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/hydrohq/hydrogo/main/docs/schemas/content-type.schema.json id: recipes label: Recipes description: Dishes shown on the food blog. title_field: title public: true # serve published records at /api/v1/content/recipes drafts: true # records start as drafts; the public API returns only published ones revisions: true # keep the last 50 versions, with compare and restore fields: - {id: title, label: Title, kind: text, required: true, list: true, searchable: true} - {id: course, label: Course, kind: select, options: [starter, main, dessert], list: true, filterable: true} - {id: photo, label: Photo, kind: media} - {id: author, label: Author, kind: relation, to: team} - {id: method, label: Method, kind: richtext, searchable: true} ``` Restart `hydro serve` after adding or changing a file. Unknown YAML keys are rejected, so a typo fails loudly instead of being ignored. ## Type keys | Key | Required | Meaning | | --- | --- | --- | | `id` | yes | Lowercase letters, digits and underscores, starting with a letter (max 63 characters). Becomes the URL segment in `/api/v1/content/` and `/admin/content/`. | | `label` | yes | Human name shown in the admin. | | `fields` | yes | At least one field. See [Fields](https://hydrogo.dev/docs#content-types/fields). | | `description` | no | Help text under the title in the admin. | | `group` | no | Admin navigation group that contains this type. | | `icon` | no | Icon for the navigation entry. | | `title_field` | no | Field whose value is shown as the record title. | | `public` | no | Serve published records at `/api/v1/content/`. Types that are not public answer 404, exactly like unknown types. | | `drafts` | no | Add a draft or published status. New records start as drafts and the public API never returns drafts. | | `revisions` | no | Keep the last 50 saved versions of each record. Required when any field is `localized`. | > [!NOTE] These field ids are reserved because the storage layer adds them to every record: `id`, `version`, `created_at`, `updated_at` and `locale`. ## YAML tips - The compact flow style `- {id: title, label: Title, kind: text}` keeps one field per line; use the expanded block style when a field has many options. - Comments are welcome and are the best place to explain why a field exists. - Commit the files. A content type file is part of your data model. > [!TIP] The complete reference is the header comment of `internal/contenttypes/definition.go` in the repository. Everything on these pages is derived from it. --- # Fields *Section: Content types* *Every field kind and flag, which flags apply to which kinds, and how each kind appears in the admin and the API.* ## Field keys | Key | Applies to | Meaning | | --- | --- | --- | | `id` | all | Identifier, same pattern as the type id. Used as the JSON key. | | `label` | all | Label in forms and list columns. | | `kind` | all | One of the kinds below. | | `required` | all | The record cannot be saved without a value. | | `unique` | all | No two records may share the value. Not combinable with `localized`. | | `options` | `select` | List of allowed values. | | `to` | `relation` | Id of the target content type. | | `many` | `relation` | `true` lets one record link to several targets. | | `min`, `max` | `number` | Inclusive numeric bounds. | | `list` | all | Show as a column in the admin list. List columns are sortable by clicking the heading. | | `searchable` | `text`, `textarea`, `richtext`, `select` | Include in the admin list search box and the public API `?search=`. | | `filterable` | `text`, `textarea`, `select`, `number` | Add a filter to the admin list and allow `?filter[field]=` on the public API. | | `localized` | `text`, `textarea`, `richtext`, `media` | Keep one value per site locale. See [Localized fields](https://hydrogo.dev/docs#multi-language/content-locales#localized-fields-in-content-types). | ## Field kinds | Kind | Stores | Admin control | API value | | --- | --- | --- | --- | | `text` | Short string | Single-line input | String | | `textarea` | Long plain text | Multi-line input | String | | `richtext` | Formatted text | Rich text editor | HTML string | | `number` | Number | Number input | Number | | `boolean` | True or false | Checkbox or switch | Boolean | | `date` | Calendar date | Date picker | Date string | | `datetime` | Date and time | Date and time picker | Timestamp string | | `select` | One of `options` | Dropdown | String | | `media` | Reference to a media item | Media library picker | Media reference | | `relation` | Reference to another record | Record picker | Record id (array with `many`) | ## Examples of each kind ```yaml fields: - {id: title, label: Title, kind: text, required: true, unique: true, list: true, searchable: true} - {id: summary, label: Summary, kind: textarea, searchable: true} - {id: body, label: Body, kind: richtext} - {id: price, label: Price, kind: number, min: 0, max: 10000, list: true, filterable: true} - {id: in_stock, label: In stock, kind: boolean} - {id: released_on, label: Released, kind: date} - {id: starts_at, label: Starts at, kind: datetime} - {id: status_note, label: Level, kind: select, options: [low, medium, high], filterable: true} - {id: cover, label: Cover image, kind: media} - {id: related, label: Related, kind: relation, to: recipes, many: true} ``` ## Validation - `required`, `unique`, `min`, `max` and `options` are enforced by the server on every write, from the admin and the API alike. - Validation failures answer `422` with a `fields` map, for example `{"title": "required"}`. See [Errors](https://hydrogo.dev/docs#rest-api/overview#errors). - A field may not be both `localized` and `unique`, `searchable` or `filterable`; the loader rejects such files. > [!WARNING] Changing a field's `kind` after records exist is a data migration, not a rename. Add a new field, move the data, then remove the old one. --- # Relations, drafts and revisions *Section: Content types* *Link records to each other, add a publishing workflow and keep a history of changes.* ## Relations A `relation` field points at another content type by its id. Use `many: true` for a list of targets. ```yaml # content-types/team.yaml (target) id: team label: Team title_field: name public: true fields: - {id: name, label: Name, kind: text, required: true, list: true} # content-types/recipes.yaml (source) fields: - {id: author, label: Author, kind: relation, to: team} - {id: tasters, label: Tasters, kind: relation, to: team, many: true} ``` In the API a relation is the id of the target record; fetch it with `GET /api/v1/content/team/` or list the target type and join on the client. ## Drafts With `drafts: true` every record has a status field: | Status | Visible in the public API | How it changes | | --- | --- | --- | | draft | No. A draft id answers 404. | New records start here. **Move to draft** in the admin returns a record here. | | published | Yes | **Publish** in the admin. | Without `drafts`, a record is live as soon as it is saved (when the type is `public`). ## Revisions With `revisions: true` HydroGo keeps the last 50 saved versions of every record. The edit page lists them; each can be compared with the current record and restored. Restoring saves a new version and keeps the current status, so history is never rewritten. ## Concurrent editing Records carry a `version` number. An update must send the version it was based on; if someone else saved in between, the server rejects the write with a conflict instead of overwriting their change. The admin shows who else is editing a record. > [!TIP] Recommended defaults for editorial content: `drafts: true` and `revisions: true`. For settings-like singletons (site settings, navigation) neither is usually needed. --- # Tooling and schema *Section: Content types* *The hydro content-type commands, JSON Schema for editor completion, and the stock types that ship with HydroGo.* ## Create and validate ```bash hydro content-type new recipes # writes content-types/recipes.yaml hydro content-type validate # loads every file and reports errors hydro content-type new events --dir my-types hydro content-type validate --dir my-types ``` | Command | What it does | | --- | --- | | `hydro content-type new ID [--dir content-types]` | Writes a skeleton file for the new type. `ID` follows the id pattern. | | `hydro content-type validate [--dir content-types]` | Loads every `*.yaml` file in the directory and reports the first problem per file: unknown keys, bad kinds, missing relation targets, invalid ids. | > [!TIP] Run `hydro content-type validate` in CI so a broken YAML never reaches a server. ## Editor completion with JSON Schema A machine-readable schema for content type files is in the repository at `docs/schemas/content-type.schema.json`. Add this line as the first line of a YAML file and editors with the YAML language server (VS Code YAML extension, for instance) give completion, hover docs and inline validation: ```yaml # yaml-language-server: $schema=https://raw.githubusercontent.com/hydrohq/hydrogo/main/docs/schemas/content-type.schema.json ``` Offline, point at a local copy instead: `# yaml-language-server: $schema=../schemas/content-type.schema.json`. ## Stock content types `hydro new --preset ...` copies stock types into your project. They are examples; edit or delete freely. | Type | Purpose | | --- | --- | | `site_settings` | Brand name, tagline and footer texts (a single record). | | `site_navigation` | Header and footer menu links. | | `faq` | Questions and answers with category. | | `team` | People with localized role and bio. | | `testimonials` | Customer quotes. | | `comments` | Reader comments that wait for moderation (draft to published). | | `galleries` | Photo galleries. | | `homepage_sections`, `masthead`, `columnists`, `official_announcements`, `ads`, `polls`, `videos` | Building blocks of the news portal preset. | > [!NOTE] Some news-portal stock types ship with Turkish labels because they were made for a Turkish news site. Edit the `label` and `description` values to your language. ## Admin API for content types The admin lists and manages content types at `/api/v1/admin/content-types`, and records at `/api/v1/admin/content/{type}`. All of them need an authenticated credential; see [Admin API](https://hydrogo.dev/docs#rest-api/admin-api). ================================================================================ # SECTION: COMMERCE ================================================================================ --- # Commerce Overview *Section: Commerce* *How HydroGo models catalogs, carts, checkout, inventory reservation, and payment providers in a single compiled Go binary.* The `commerce` module adds a shopping cart, guest and member checkout, order lifecycle management, payment gateway integration, shipping provider adapters, and automated invoicing on top of a catalog defined as standard content types. Commerce is opt-in: activate it by adding `commerce` to the `modules:` list in `config.yaml`, or initialize a new project with `hydro new mysite --preset commerce`. ## Catalog as Content Types Unlike rigid legacy commerce platforms, products and variants in HydroGo are declarative YAML schemas in `content-types/commerce/`: - `products.yaml`: Main product attributes (title, slug, description, base price, categories). - `variants.yaml`: Color, size, SKU, barcode, and per-variant pricing overrides. - `categories.yaml`: Product groupings, hierarchical trees, and featured banners. - `product_images.yaml`: Gallery photos with focal point cropping and WebP optimization. Because the catalog uses standard content types, products automatically receive admin management screens under `/admin/commerce` and public cached read APIs under `/api/v1/content/products`. ## Transactional SQL Core While the catalog is modeled as content types, operational transactional data lives in dedicated SQL tables optimized for ACID concurrency: | Component | Storage | Concurrency Guarantee | | --- | --- | --- | | Catalog (Products, Categories) | YAML + Content Engine | Cached edge reads, instant zero-build publish | | Cart & Line Items | SQL (`commerce_carts`, `items`) | Session or anonymous cart cookie with atomic updates | | Inventory & Stock | SQL (`commerce_stock`) | Deterministic variant locking to eliminate 40P01 deadlocks | | Orders & Payments | SQL (`commerce_orders`) | Atomic `ClaimCart` serialization with idempotent retry keys | > [!TIP] HydroGo serializes concurrent multi-tab checkout attempts via atomic cart claims, preventing duplicate orders or inventory race conditions. --- # Cart & Checkout API *Section: Commerce* *REST endpoints for customer carts, server-side pricing verification, and atomic checkout submission.* ## Public Cart & Checkout Endpoints | Method | Endpoint | Description | | --- | --- | --- | | `GET` | `/api/v1/commerce/cart` | Retrieve the active shopper's cart (via member auth or cart session cookie). | | `POST` | `/api/v1/commerce/cart/items` | Add a SKU/variant to cart. Prices are always calculated server-side. | | `DELETE` | `/api/v1/commerce/cart/items/{id}` | Remove a line item from the cart. | | `GET` | `/api/v1/commerce/stock/{variantID}` | Check real-time available and reserved inventory for a variant. | | `GET` | `/api/v1/commerce/currencies` | List supported display currencies and configured exchange rates. | | `GET` | `/api/v1/commerce/checkout-config` | Retrieve active payment providers, shipping rules, and tax settings. | | `POST` | `/api/v1/commerce/checkout` | Place order from current cart. Supports `idempotency_key` header. | | `GET` | `/api/v1/commerce/orders/{id}` | Inspect placed order status, items, and tracking info (owner only). | | `POST` | `/api/v1/commerce/orders/{id}/payment` | Initiate payment session or retry payment for an existing unpaid order. | ## Server-Side Price Integrity > [!WARNING] The client never specifies prices. When calling `POST /api/v1/commerce/cart/items`, the frontend sends only `{variant_id, quantity}`. HydroGo inspects the database catalog, verifies active promotional rules, and calculates line totals server-side. ## Idempotent Checkout Example ```bash curl -X POST https://cms.example.com/api/v1/commerce/checkout \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8a7f92b4-5c1e-43a9-b36d-1092837465ab" \ -d '{ "customer": { "name": "Ahmet Yılmaz", "email": "ahmet@example.com", "phone": "+905551234567" }, "shipping_address": { "address_line1": "Büyükdere Cad. No: 122", "city": "İstanbul", "district": "Şişli", "postal_code": "34394", "country": "TR" }, "payment_method": "paytr" }' ``` ## Admin Commerce API Requires `admin.commerce.read` or `admin.commerce.write` permissions: | Method | Endpoint | Purpose | | --- | --- | --- | | `GET` | `/api/v1/admin/commerce/orders` | Search, filter, and paginate through customer orders. | | `GET` | `/api/v1/admin/commerce/orders/{id}` | Detailed order breakdown with line items and audit log. | | `GET` | `/api/v1/admin/commerce/stock` | Overview of inventory levels across all catalog variants. | | `POST` | `/api/v1/admin/commerce/stock` | Adjust physical stock count or trigger inventory replenishment. | | `POST` | `/api/v1/admin/commerce/discount-codes` | Create percentage or fixed-amount promotional voucher codes. | | `POST` | `/api/v1/admin/commerce/orders/{id}/shipment` | Mark an order as dispatched and attach tracking details. | --- # Payments & Integrations *Section: Commerce* *Pluggable enterprise gateways: Hosted 3DS checkout, bank wire transfers, multi-carrier shipping, and fiscal invoicing.* ## Payment Orchestration HydroGo's payment module supports multi-provider checkout with automated webhook callbacks, idempotent transaction processing, and fractional currency rounding: - **Hosted Card Gateways:** 3D Secure card processing, installment matrices, and server-to-server HMAC-verified callbacks at `/api/v1/commerce/payments/{provider}/callback`. - **Tokenized Recurring Billing:** Secure customer card tokenization and automated subscription renewals via `/api/v1/payments/{provider}/webhook`. - **Direct Bank Transfer (Wire / EFT):** Manual wire transfer with order reference matching. Operators confirm receipt in one click via `POST /api/v1/admin/commerce/orders/{id}/confirm-transfer`. > [!NOTE] Basket totals are calculated in integer currency cents and assign any fractional penny remainder to the largest line item, ensuring third-party provider sums strictly match TotalCents without rounding discrepancies. ## Multi-Carrier Shipping Connectors The built-in logistics adapter connects to multi-carrier shipping APIs and parcel brokers: - **Multi-carrier rate comparison:** Fetches real-time shipping quotes across connected couriers inside the admin order drawer. - **One-click barcode PDF:** Automatically generates shipping barcode labels and tracking IDs upon order fulfillment. - **Automated delivery webhooks:** Carrier webhook endpoints update order transit and delivery milestones in real time. ## Automated Fiscal & ERP Invoicing The accounting integration issues electronic invoices and syncs ledgers as transactions settle: - **Automated invoice drafts:** Pushes validated customer tax IDs, line items, and tax rates directly to connected ERP systems. - **Date & Ledger Standardization:** Invoices are pinned to the operational timezone so financial records remain audit-consistent. ## Commerce Reconcile CLI HydroGo includes a built-in consistency engine to detect stale unpaid reservations or missed webhooks: ```bash hydro commerce reconcile --hold 30m --stale-after 15m --limit 100 --json ``` The reconciliation sweep also runs automatically in the background every 5 minutes, logging warnings if an order status is out of sync with payment providers. ================================================================================ # SECTION: REST API ================================================================================ --- # API overview *Section: REST API* *Base URL, the response envelope, the error shape, pagination and status codes shared by every endpoint.* The API is plain JSON over HTTP under `/api/v1`. Public endpoints return published content without credentials; admin endpoints live under `/api/v1/admin` and need one of the credentials in [Authentication](https://hydrogo.dev/docs#rest-api/authentication). ```bash curl -s https://cms.example.com/api/v1/news?locale=en\&limit=3 ``` ## Success envelope Every successful JSON response wraps its payload in `data`. List endpoints add `metadata` with paging information. ```json { "data": [ { "id": "…", "slug": "hello-world", "title": "Hello world" } ], "metadata": { "limit": 20, "offset": 0, "has_more": true } } ``` | Metadata field | Meaning | | --- | --- | | `limit` | Page size used for this response. | | `offset` | Records skipped (offset-paged lists). | | `total` | Total matching records, when it is cheap to know. | | `has_more` | `true` when another page exists. | | `next_cursor` | Opaque cursor for cursor-paged lists; pass it back to continue. | | `locale` | The locale served (content API). | ## Errors Errors use a single shape, so clients can handle them in one place. Always show or log the `request_id`: it is also sent in the `X-Request-ID` response header and lets you find the matching server log line. ```json { "code": "unprocessable_entity", "message": "validation failed", "request_id": "7c2f4a1e9b3d", "fields": { "title": "required" } } ``` | HTTP status | `code` | When | | --- | --- | --- | | 400 | `bad_request` | Malformed JSON, invalid query parameter (for example `limit` out of range), unknown locale. | | 401 | `unauthorized` | Missing, expired or invalid credential. | | 403 | `forbidden` | Authenticated but not allowed (role, permission, scope or missing CSRF token). | | 404 | `not_found` | Unknown route, record, or a type that is not `public`. | | 409 | `conflict` | Version conflict: someone saved a newer version first. | | 413 | `request_entity_too_large` | Body larger than the endpoint accepts (JSON bodies default to 1 MiB). | | 422 | `unprocessable_entity` | Validation failed. `fields` maps field ids to codes such as `required`, `invalid`, `taken`. | | 429 | `too_many_requests` | Rate limited. Honour the `Retry-After` header (seconds). | | 500 / 503 | `internal_server_error` / `service_unavailable` | Server problem. Report the `request_id`. | ## Pagination - `limit` accepts 1 to 100. The default is 20 for pages and news and 25 for content types. - Content types use `offset`; continue while `metadata.has_more` is `true`. - Always loop on `has_more` rather than computing pages from `total`, which may be absent. ```ts const all = []; let offset = 0; for (;;) { const res = await fetch(`${origin}/api/v1/content/faq?limit=100&offset=${offset}`); const body = await res.json(); all.push(...body.data); if (!body.metadata.has_more) break; offset += body.metadata.limit; } ``` ## Conventions - Field names are `snake_case`; timestamps are RFC 3339 strings in UTC. - Ids are opaque strings. Do not parse them. - Query strings are strict: unsupported parameters on the content API answer `400` instead of being ignored. - Browsers on another origin can read published content only if the origin is listed in `security.public_api_origins`. Server-side callers are not affected. > [!NOTE] The machine-readable contract is the OpenAPI baseline kept in the repository under `docs/api/`. CI compares every change to it, so accidental breaking changes are caught before release. Generate a typed SDK with `hydro sdk generate`. --- # Authentication *Section: REST API* *Session cookie with CSRF, Bearer tokens and API keys, how to obtain each, and which endpoints need which.* Public read endpoints need no credential. Everything under `/api/v1/admin` works with **any of three credentials** through the same middleware, so a script can do what the admin panel does. | Credential | Header or cookie | Best for | | --- | --- | --- | | Session | Cookie `hydro_session` plus `X-CSRF-Token` on unsafe methods | The admin SPA and same-origin browser code. | | Bearer token | `Authorization: Bearer hyd_…` | Personal scripts and tooling acting as you. | | API key | `X-API-Key: hyd_…` | Frontends, SSR servers and CI. Read-only. | ## Session cookie and CSRF Sign in with e-mail, password and, if enabled, a TOTP code. The server sets an HTTP-only `hydro_session` cookie and a `hydro_csrf` cookie. For every `POST`, `PUT`, `PATCH` and `DELETE`, echo the CSRF token in the `X-CSRF-Token` header. The token is returned by `GET /csrf-token` (`{"token": "…"}`) and by `GET /api/v1/admin/me`. **curl**: ```bash # 1. sign in and keep the cookies curl -c jar.txt -X POST https://cms.example.com/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"you@example.com","password":"…","mfa_code":""}' # 2. fetch the CSRF token TOKEN=$(curl -s -b jar.txt -c jar.txt https://cms.example.com/csrf-token | jq -r .token) # 3. make a state-changing request curl -b jar.txt -X POST https://cms.example.com/api/v1/admin/pages \ -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \ -d '{ … }' ``` **TypeScript**: ```ts // In the browser the cookies are sent automatically. const { token } = await (await fetch('/csrf-token', { credentials: 'same-origin' })).json(); await fetch('/api/v1/admin/pages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token }, body: JSON.stringify({ /* … */ }), }); ``` ## Bearer tokens (personal) A personal token acts as its owner with admin-level API access. Create one in the admin under **My Account** or via the API from a browser session (tokens cannot mint tokens). The plaintext token is shown **once**. ```bash curl -X POST https://cms.example.com/api/v1/admin/account/tokens \ -b jar.txt -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"deploy script","expires_in_days":90}' # -> { "data": { "token": "hyd_…", "item": { "id": "…", "prefix": "hyd_ab12" } } } curl https://cms.example.com/api/v1/admin/me -H 'Authorization: Bearer hyd_…' ``` `expires_in_days` is optional (omit or `0` for no expiry, maximum 3650). List with `GET /api/v1/admin/account/tokens` and revoke with `DELETE /api/v1/admin/account/tokens/{id}`. ## API keys API keys are for machines. They are **read-only** and carry one scope. Only administrators create them (`POST /api/v1/admin/api-keys`, also available in the admin). | Scope | Allows | | --- | --- | | `read` | Read endpoints. | | `preview` | Read endpoints, and viewing unpublished content once handlers honour it (draft preview). | ```bash curl -X POST https://cms.example.com/api/v1/admin/api-keys \ -b jar.txt -H "X-CSRF-Token: $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"frontend ssr","scope":"read","expires_in_days":365}' curl https://cms.example.com/api/v1/admin/me -H 'X-API-Key: hyd_…' ``` > [!NOTE] A HydroGo token header (`Authorization: Bearer hyd_…` or `X-API-Key`) is authoritative: when one is present the session cookie is ignored. Because these requests do not rely on cookies, they do not need a CSRF token. ## Staff mobile tokens Native apps can sign staff in with `POST /api/v1/mobile/auth/login` (`email`, `password`, optional `mfa_code`, `device_id`, `device_name`, `platform`) and renew with `/api/v1/mobile/auth/refresh`. `revoke` and `revoke-all` end one or all device sessions. Members use the parallel `/api/v1/mobile/member/auth/*` family, see [Member API](https://hydrogo.dev/docs#rest-api/members-api). > [!WARNING] Never ship an API key or token in browser code. Use them from a server (SSR loader, serverless function, build step) and keep them in environment variables. ## Credential cheat sheet | I want to… | Use | | --- | --- | | Read published content in my SSR app | No credential needed; an API key `read` is optional and lets you separate traffic. | | Preview drafts in my frontend | API key `preview` or a signed preview link. See [Draft previews](https://hydrogo.dev/docs#rest-api/previews). | | Script imports and edits | Personal Bearer token. | | Let the admin SPA work | Session cookie plus CSRF (automatic). | --- # Pages and news *Section: REST API* *List and read published pages and news articles, with curl and @hydrogo/client examples.* ## Pages | Method and path | Description | | --- | --- | | `GET /api/v1/pages?locale=en&limit=20` | List published page summaries. `limit` is 1 to 100. | | `GET /api/v1/pages/by-slug?locale=en&slug=about` | One published page, with query-string parameters. | | `GET /api/v1/pages/{locale}/{slug}` | The same page with path segments. | **curl**: ```bash curl 'https://cms.example.com/api/v1/pages/by-slug?locale=en&slug=about' ``` **@hydrogo/client**: ```ts import { createHydroGoClient } from '@hydrogo/client'; const hydro = createHydroGoClient({ origin: 'https://cms.example.com' }); const about = await hydro.getPage('en', 'about'); const summaries = await hydro.listPages('en', 100); ``` ```json { "data": { "id": "pg_01", "locale": "en", "slug": "about", "title": "About us", "body": "

…

", "blocks": [ { "type": "hero", "payload": { "heading": "Hello" } } ], "status": "published", "version": 7, "revision": 12, "published_revision": 12, "publication_generation": 3, "created_at": "2026-01-10T09:00:00Z", "updated_at": "2026-02-02T14:30:00Z" } } ``` List responses contain summaries only (`id`, `locale`, `slug`, `title`, `revision`, `publication_generation`, `updated_at`), which makes them cheap to use for navigation and sitemaps. Fetch the full page by slug. ## News | Method and path | Description | | --- | --- | | `GET /api/v1/news?locale=en&limit=20&category=product` | List published articles. `locale` is required; `category` is optional. | | `GET /api/v1/news/by-slug?locale=en&slug=launch` | One published article. | | `GET /api/v1/news/{locale}/{slug}` | The same article with path segments. | | `POST /api/v1/news/views`, `GET /api/v1/news/views` | Record and read article view counts. | **curl**: ```bash curl 'https://cms.example.com/api/v1/news?locale=en&limit=5&category=product' ``` **@hydrogo/client**: ```ts const latest = await hydro.listNews('en', 5); const article = await hydro.getNews('en', 'launch'); console.log(article.title, article.updated_at); ``` An article carries `id`, `locale`, `slug`, `title`, `body` and, when set, `excerpt`, `category`, `author_name`, `tags`, `cover_media_id`, `seo_title`, `seo_description`, `featured`, `scheduled_at`, `expires_at`, plus `revision` and `updated_at`. ## Scheduled publishing and retraction News supports scheduled publishing (`scheduled_at`) and expiry (`expires_at`). Articles appear in the API when their schedule arrives and disappear when they are retracted or expire, without any rebuild. Each of these events can fire a [webhook](https://hydrogo.dev/docs#frontends/caching-and-webhooks). > [!TIP] Use `GET /api/v1/pages?locale=…` and `GET /api/v1/news?locale=…` to generate your sitemap and navigation. They return `updated_at` for ``. ## Errors you will see | Case | Status | | --- | --- | | Unknown slug or unpublished item | 404 | | Missing `locale` on news list | 400 | | `limit` outside 1 to 100 | 400 | --- # Content API *Section: REST API* *Read published records of your YAML content types, with search, filters, sorting and locale.* Every content type with `public: true` is readable without credentials. Types without it, and unknown types, both answer `404`. | Method and path | Description | | --- | --- | | `GET /api/v1/content/{type}` | List published records. | | `GET /api/v1/content/{type}/{id}` | One published record. A draft id answers 404. | ## Query parameters | Parameter | Meaning | | --- | --- | | `limit` | 1 to 100, default 25. | | `offset` | Records to skip, default 0. | | `sort` | A field id, or `created_at` / `updated_at`. Prefix with `-` for descending: `sort=-updated_at`. | | `search` | Case-insensitive contains match across all fields marked `searchable`. If the type has none, the request answers 400. | | `filter[field]` | Exact match on a field marked `filterable`, for example `filter[category]=billing`. Several filters combine with AND. | | `locale` | A configured site locale. Localized fields return that locale and fall back to the default per field. Unknown locale answers 400. | > [!NOTE] Each parameter may appear once. Unsupported parameters, repeated parameters and filters on fields that are not `filterable` answer 400 with a message naming the problem. ## Examples **curl**: ```bash # newest FAQ entries about refunds in the billing category curl -G 'https://cms.example.com/api/v1/content/faq' \ --data-urlencode 'search=refund' \ --data-urlencode 'filter[category]=billing' \ --data-urlencode 'sort=-updated_at' \ --data-urlencode 'limit=10' ``` **@hydrogo/client**: ```ts type Faq = { id: string; question: string; answer: string; category: string }; const res = await hydro.listContent('faq', { search: 'refund', filter: { category: 'billing' }, sort: '-updated_at', limit: 10, locale: 'en', }); res.data.forEach((f) => console.log(f.question)); const one = await hydro.getContent('faq', res.data[0].id, 'en'); ``` ```json { "data": [ { "id": "rec_8f2", "question": "How do refunds work?", "answer": "

…

", "category": "billing", "created_at": "2026-03-01T10:00:00Z", "updated_at": "2026-03-04T08:12:00Z", "locale": "en" } ], "metadata": { "limit": 10, "offset": 0, "has_more": false, "locale": "en" } } ``` ## Notes - Rich text fields are returned as HTML ready to render. - Each record also contains `id`, `created_at`, `updated_at` and, when localization is active, `locale`. - Search, filters and sorting always use the default locale's values, even when `locale` asks for another. - Types with `drafts: true` only expose published records; the list is filtered server-side. > [!WARNING] Keep secrets and private data out of `public: true` types. Everything in a public type is world-readable once published. --- # Other public endpoints *Section: REST API* *Media, site settings, navigation, redirects, forms, search, sitemaps, Open Graph images and operational endpoints that need no credentials.* ## Media | Path | Description | | --- | --- | | `GET /media/{id}` | Serve a public media item or one of its derivatives (WebP, thumbnails). | Media is referenced by id from `media` fields and from page and news content. Use `media.cdn_base_url` to serve files from a CDN origin, and `media.hotlink_protection` to stop other sites embedding them. ## Site | Path | Description | | --- | --- | | `GET /api/v1/site/settings` | Public site settings such as business profile and social links. | | `GET /api/v1/site/navigation/{locale}` | Navigation menus for a locale. | | `GET /api/v1/site/redirects` | Configured redirects so your frontend can honour them. | ## Forms Forms are defined in the admin (**Site > Forms**) and submissions land in the **Inbox**. | Path | Description | | --- | --- | | `GET /api/v1/forms/{form}` | The form definition (fields and labels). | | `POST /api/v1/forms/{form}/submit` | Submit a form. Supports Cloudflare Turnstile when its keys are configured. | | `POST /api/v1/forms/{form}/view` | Record that a form was displayed. | ## Search | Path | Description | | --- | --- | | `GET /api/v1/search/autocomplete` | Suggestions across published content (needs the search capability to be enabled). | ## SEO helpers | Path | Description | | --- | --- | | `GET /sitemap.xml`, `/sitemap-index.xml`, `/sitemap-pages.xml`, `/sitemap-articles.xml` | XML sitemaps built from published content. | | `GET /og/{locale}/{slug}` | Generated Open Graph image for a page or article. | ## Operations | Path | Description | | --- | --- | | `GET /healthz` | Liveness: the process is up. | | `GET /readyz` | Readiness: the process can serve (database reachable). | | `GET /metrics` | Prometheus metrics. | | `GET /csrf-token` | The current CSRF token as JSON. | > [!TIP] Wire `/readyz` into your load balancer or orchestrator, and `/healthz` into restart policies. See [Monitoring](https://hydrogo.dev/docs#deployment/monitoring). --- # Member API *Section: REST API* *Sign-up, sign-in and profile endpoints for the visitors of your site, designed for mobile apps and token-based clients.* Members are visitor accounts, separate from admin users. The token API lives under `/api/v1/mobile/member` and never uses browser cookies (the client sends `credentials: omit`). It needs the `members` module. | Method and path | Description | | --- | --- | | `POST /api/v1/mobile/member/auth/register` | Create an account; returns a token pair. | | `POST /api/v1/mobile/member/auth/login` | Sign in; returns a token pair. | | `POST /api/v1/mobile/member/auth/refresh` | Exchange a refresh token for a new pair. | | `POST /api/v1/mobile/member/auth/revoke` | Sign out this device. | | `POST /api/v1/mobile/member/auth/revoke-all` | Sign out every device. | | `GET /api/v1/mobile/member/auth/devices` | List signed-in devices. | | `GET /api/v1/mobile/member/me` | The member profile. | | `POST` / `DELETE /api/v1/mobile/member/push/subscription` | Register or remove a push subscription. | ## Sign in **curl**: ```bash curl -X POST https://cms.example.com/api/v1/mobile/member/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"reader@example.com","password":"…","device_id":"phone-1","device_name":"Pixel","platform":"android"}' ``` **@hydrogo/client**: ```ts const tokens = await hydro.memberLogin({ email: 'reader@example.com', password: '…', device_id: 'phone-1', device_name: 'Pixel', platform: 'android', }); const me = await hydro.memberMe(tokens.access_token); const renewed = await hydro.memberRefresh(tokens.refresh_token); await hydro.memberLogout(tokens.access_token, /* allDevices */ false); ``` ```json { "access_token": "…", "refresh_token": "…", "token_type": "Bearer", "expires_in": 900, "expires_at": "2026-04-01T12:15:00Z" } ``` > [!NOTE] Member endpoints return their own documents (token pair, profile) directly rather than inside `data`. @hydrogo/client already handles each shape for you. ## Authenticated calls ```bash curl https://cms.example.com/api/v1/mobile/member/me -H 'Authorization: Bearer ' ``` Browser-based member sign-in (cookie sessions) and OAuth routes exist at `/member/login`, `/member/register` and `/member/oauth/{provider}/start`; they are meant for same-origin apps. The mobile OpenAPI contracts are stored in the repository under `docs/api/`. --- # Admin API *Section: REST API* *A map of the /api/v1/admin endpoints. The admin panel is a client of this API, so anything it does you can script.* All endpoints below require a credential (see [Authentication](https://hydrogo.dev/docs#rest-api/authentication)), respect roles and permissions, and use the same envelope and error format as the public API. Writes use optimistic concurrency: send the record's current `version` and expect `409` if it changed. > [!WARNING] This page is an orientation, not the contract. The authoritative description is the OpenAPI document: read `docs/api/` in the repository or generate a client with `hydro sdk generate`. ## Identity and account | Path | Description | | --- | --- | | `GET /api/v1/admin/me` | Current user, role, permissions, permission catalog, CSRF token. | | `PUT /api/v1/admin/account/locale` | Set the signed-in user's admin interface language. | | `GET` / `POST /api/v1/admin/account/tokens` | List or create personal tokens. `DELETE .../{id}` revokes. | | `POST /api/v1/admin/account/mfa/enroll`, `enable`, `disable` | Manage TOTP for your own account. | | `GET /api/v1/admin/i18n/{locale}` | UI translations for the admin panel (`en`, `tr`). | ## Content | Path | Description | | --- | --- | | `/api/v1/admin/pages` | List, create, bulk actions, import, read, update, `publish`, `translate`, revisions (`/{id}/revisions`, `.../restore`), presence and `preview-link`. | | `/api/v1/admin/news` | List, create, read, update, taxonomy (categories and tags). | | `/api/v1/admin/media` | Upload, list, edit and delete media. | | `/api/v1/admin/members` | Member management. | | `/api/v1/admin/content-types` | List, create and delete content type definitions. | | `/api/v1/admin/content/{type}` | Records of a content type: list with `search`, `sort`, `filter[f]`, `status` and `locale`; create; read; update; delete; `workflow`; revisions; per-locale values. | ```bash # publish a page with a Bearer token curl -X POST https://cms.example.com/api/v1/admin/pages/PAGE_ID/publish \ -H 'Authorization: Bearer hyd_…' # list drafts of a content type curl 'https://cms.example.com/api/v1/admin/content/faq?status=draft&limit=50' \ -H 'Authorization: Bearer hyd_…' ``` ## Site | Path | Description | | --- | --- | | `/api/v1/admin/site/settings`, `business-profile`, `social-links`, `maintenance`, `staff-notice` | Read with `GET`, change with `PUT`. | | `/api/v1/admin/site/navigation/{locale}` | Menus per locale. | | `/api/v1/admin/site/redirects` | CRUD plus `POST .../import`, and `site/not-found` to turn 404s into redirects. | | `/api/v1/admin/site/forms`, `site/inbox` | Forms and submissions, with CSV and XLSX export. | | `/api/v1/admin/site/translations/{locale}` | Site-level translation strings. | ## People and security | Path | Description | | --- | --- | | `/api/v1/admin/users` | List and read users; `POST .../invites`, `.../{id}/role`, `.../permissions`, `.../disable`, `.../password-reset-link`. | | `/api/v1/admin/api-keys` | List, create and revoke API keys. | | `/api/v1/admin/audit` | Search the audit trail. | | `/api/v1/admin/security/ip-bans` | List, add and remove IP bans. | ## System | Path | Description | | --- | --- | | `GET /api/v1/admin/dashboard` | Counts and recent activity for the dashboard. | | `GET /api/v1/admin/system/health`, `info`, `metrics` | Runtime status. | | `/api/v1/admin/backups` | List and trigger backups. | | `GET /api/v1/admin/search` | Global admin search. | | `GET /api/v1/admin/reports`, `notifications`, `schema`, `permissions` | Reports, in-app notifications, the schema that drives generic screens, the permission catalog. | --- # Draft previews *Section: REST API* *Show unpublished pages and articles in your own frontend with signed, short-lived preview links.* Editors often need to see a draft inside the real site before they publish. HydroGo issues **revision-bound, signed preview tokens**; your frontend uses them to fetch the draft. ## How it works - An editor asks the admin for a preview link: `POST /api/v1/admin/pages/{id}/preview-link`. - The response contains a signed token bound to that revision. - Your frontend calls `GET /api/v1/pages/{id}/preview?token=` (news has the matching `/api/v1/news/{id}/preview`) from the server side and renders the draft. - Responses carry `Cache-Control: private, no-store`, so previews are never cached by a CDN. ```ts // SvelteKit: src/routes/preview/[id]/+page.server.ts import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; export async function load({ params, url }) { const token = url.searchParams.get('token'); if (!token) error(400, 'Missing preview token'); const res = await fetch( `${env.HYDRO_API_ORIGIN}/api/v1/pages/${params.id}/preview?token=${encodeURIComponent(token)}`, ); if (!res.ok) error(res.status, 'Preview unavailable'); const { data } = await res.json(); return { page: data }; } ``` > [!WARNING] Set `security.preview_signing_key` (at least 32 bytes, or `HYDRO_PREVIEW_SIGNING_KEY`) in production so preview links survive restarts and cannot be forged. Do not index preview routes: serve them with `X-Robots-Tag: noindex`. > [!TIP] API keys with the `preview` scope are the other way to read unpublished content from a trusted server. ================================================================================ # SECTION: ADMIN PANEL ================================================================================ --- # Admin panel overview *Section: Admin panel* *Sign in, find your way around the embedded admin at /admin, and understand what each role can see.* The admin panel is a Svelte 5 single-page application compiled into the `hydro` binary and served at `/admin`. It talks to the same REST API your frontends use, so there is nothing extra to deploy. ## Signing in 1. Open `/admin` on your HydroGo origin (for a local project, `http://127.0.0.1:8080/admin`). 2. Enter the e-mail and password of an admin user. The first one is created by `hydro new --admin-email …` or `hydro create-admin`. 3. If your account has MFA enabled, enter the six-digit code from your authenticator app. 4. Forgotten password? An administrator can send you a reset link from **Users and roles**. ```bash printf '%s' "$PASSWORD" | hydro create-admin --email editor@example.com --password-stdin --config config.yaml ``` ## Layout | Area | What it does | | --- | --- | | Sidebar | Dashboard, Pages, News, Media Library, Members, your content types, Settings, Users and Roles, Telemetry, Audit Log. Collapses to icons; on phones it opens as a drawer. | | Quick Create | A button at the top of the sidebar for the most common new things: page, article, media upload. | | Search | Global admin search across content (the search box in the header). | | Notifications | In-app notices such as content awaiting attention. Daily digests can be switched on per user. | | User menu | My Account, theme (light, dark or system) and log out. | > [!NOTE] The sidebar only shows what your role and permissions allow. The server is always the authority: hiding a button is a convenience, not security. ## Roles and permissions | Role | Typical use | Access | | --- | --- | --- | | `admin` | Site owner | Everything, including users, settings and system tools. | | `editor` | Content team | Content work within the permissions granted to them. | | `viewer` | Stakeholders | Read-only. | | Permission | Allows | | --- | --- | | `content.write` | Create, edit, publish and delete articles and pages. | | `media.upload` | Upload, delete and organise files in the media library. | | `site.manage` | Change site settings, maintenance mode and redirects. | | `users.manage` | Invite staff, assign roles and permissions. | | `telemetry.read` | View runtime metrics, memory and health. | > [!TIP] Interface language and theme are personal preferences. See [Admin interface language](https://hydrogo.dev/docs#multi-language/admin-language). --- # Dashboard, pages, news and media *Section: Admin panel* *How editors create, review and publish content in the admin.* ## Dashboard The landing screen summarises your site: counts of pages, news, media and members, recent activity, active modules, and runtime figures such as uptime and request counts for people with telemetry access. ## Pages Pages are documents made of blocks. The list supports search, status filters and bulk actions. A page belongs to one locale; use **Translate** to create the same page in another locale. - **Editor.** Title, slug, body and a block editor with the catalog of block types the server offers. - **Save and publish.** Saving keeps the page as a draft. **Publish** makes the saved revision live on the next API request. - **Revisions.** Every save creates a revision. Open the revisions dialog to compare and restore. - **Preview.** Create a signed preview link to see the draft in your frontend, see [Draft previews](https://hydrogo.dev/docs#rest-api/previews). - **Presence.** When two editors open the same page, each sees who else is editing, and stale saves are rejected rather than overwriting. - **Import.** Pages can be imported in bulk. ## News News articles add editorial fields to the page model. - **Taxonomy.** Create categories and tags from the taxonomy dialog; articles can be filtered by category in the public API. - **Cover image, excerpt, author, SEO title and description.** Used by your frontend for cards and meta tags. - **Featured.** Flag articles your frontend should highlight. - **Scheduling.** Set a publish time (`scheduled_at`) and an optional expiry (`expires_at`). Scheduled items go live and expire on their own. - **Retraction.** Retract a published article to remove it from the API immediately. ## Media library - Drag and drop files onto the upload area; several files at once are fine. - Images are converted to WebP, resized into derivatives and given a BlurHash placeholder. - Set alt text and a **focal point** so crops keep the important part of the image. - Open an item to see its details, usage and public URL (`/media/{id}`). - Files can live on local disk or in S3; see [Databases and storage](https://hydrogo.dev/docs#deployment/databases-and-storage). > [!TIP] Press Publish only when you are ready: there is no build step, so the change is public right away. --- # Content type screens *Section: Admin panel* *The generic list and edit screens HydroGo generates from each YAML content type.* Every content type appears in the sidebar (grouped by its `group`) and opens `/admin/content/`. These screens are generated from the schema, so a new YAML file gets a complete admin without writing UI code. ## List view - Columns come from fields with `list: true`; click a heading to sort. - The search box searches fields with `searchable: true`. - Filters appear for fields with `filterable: true`. - Types with `drafts` show a status column and a status filter (draft or published). ## Edit view - The form controls follow the field kinds: text input, text area, rich text, number, switch, date pickers, dropdown, media picker and record picker for relations. - Validation messages come from the server and show next to the offending field. - **Publish** and **Move to draft** switch the status for types with `drafts`. - **Revisions** (when enabled) list saved versions; compare one with the current record and restore it. - **Locale tabs** appear when the site has more than one locale and the type has `localized` fields. See [Content locales](https://hydrogo.dev/docs#multi-language/content-locales). ## Content types management The **Content Types** area lists the definitions HydroGo loaded, with their fields and record counts. Definitions are authored as YAML in the project; see [Content types](https://hydrogo.dev/docs#content-types/overview). > [!NOTE] If a new YAML file does not appear, run `hydro content-type validate` and restart `hydro serve`. --- # Members, users and roles *Section: Admin panel* *Manage site visitors (members) and the staff who use the admin, including invitations, roles and per-user permissions.* ## Members Members are the registered visitors of your site. Use the Members screen to browse and manage them. Members have no access to the admin. ## Users and roles **Users and Roles** manages staff accounts. - **Invite** a person by e-mail. They receive a link to set their password (valid for the configured `invitation_ttl`). - **Change role** between `admin`, `editor` and `viewer`. - **Permissions.** Grant or revoke the five granular permissions per user. - **Disable** an account without deleting its history. - **Password reset link.** Generate a link to hand over to a colleague. - **API keys.** Administrators create read-only keys (scope `read` or `preview`) for frontends, see [Authentication](https://hydrogo.dev/docs#rest-api/authentication#api-keys). > [!WARNING] HydroGo refuses to remove the last active administrator. Keep at least two admins you can reach. ## IP bans Administrators can ban an address or CIDR range for a duration from the security area, and lift bans again. The CLI offers `hydro ban` and `hydro unban` for the same purpose. --- # Site settings and system *Section: Admin panel* *Configure the site itself: general settings, navigation, forms, translations, redirects, maintenance mode, backups and diagnostics.* ## Site settings The **Site** area has one tab per topic: | Tab | What you configure | | --- | --- | | General | Site name, default meta title and description, home route, favicon, social sharing image and the **Admin language (project default)**. | | Navigation | Menus per locale, ordered and nested. | | Forms | Public forms and their fields (contact, newsletter and so on). | | Inbox | Form submissions with notes, status, and CSV or XLSX export. | | Translations | Site-level text strings per locale. | | Maintenance | Maintenance mode with headline, message and an allow-list of IP addresses. | | Notice | A staff notice shown inside the admin, with severity. | | Business | Company profile: legal name, tax id, contact e-mail and phone, address and social links. | | Redirects | Source to target redirects, importable in bulk. | | Not found | The log of unknown URLs visitors hit; convert any of them into a redirect with one click. | ## System The **Telemetry** area is for operators. | Tab | Shows | | --- | --- | | Overview | Server information, uptime, request counts, active modules. | | Memory | Go runtime memory and the internal cache. | | Database | Connection and content statistics. | | Jobs | Background queue and job processor, including failed jobs. | | Paths | The most requested paths and their latency. | | Backups | Signed backups you can create, list and verify, plus IP access control. | ## Audit log The **Audit** screen lists who changed what and when. It is covered in [Audit trail](https://hydrogo.dev/docs#security/audit-trail). ## My Account | Section | What you can do | | --- | --- | | Profile and password | Change your credentials. | | MFA | Enrol or disable TOTP with an authenticator app. | | Sessions | See active sessions and sign out the others. | | Personal tokens | Create and revoke Bearer tokens. | | Language | Pick your own admin interface language, or follow the project default. | | Notifications | Switch the daily digest on or off. | ================================================================================ # SECTION: FRONTENDS ================================================================================ --- # @hydrogo/client *Section: Frontends* *A typed, dependency-free fetch client for the public API. Works in browsers, Node 20+, Deno, Bun and edge runtimes.* `@hydrogo/client` wraps the public endpoints with TypeScript types, request timeouts, size-capped responses and a proper error class. You can also call the API with plain `fetch`; the client just removes boilerplate. ## Install The client is not published to a public registry. Use the copy bundled with the SvelteKit starter (`hydro new --starter sveltekit` puts it at `frontend/src/lib/hydrogo.ts`), or install it from a HydroGo checkout: ```bash npm install ../path/to/hydrogo/packages/client ``` ## Create a client ```ts import { createHydroGoClient } from '@hydrogo/client'; const hydro = createHydroGoClient({ origin: 'https://cms.example.com', timeoutMs: 8000, // default 15000, maximum 120000 // fetch: customFetch, // inject your own fetch (tests, edge runtimes) }); ``` | Option | Default | Meaning | | --- | --- | --- | | `origin` | required | Base URL of your HydroGo, without a trailing path. | | `timeoutMs` | `15000` | Per-request timeout. Capped at 120000. | | `fetch` | global `fetch` | Custom fetch implementation. | | `signal` | none | An `AbortSignal` applied to every request. | ## Methods | Method | Calls | Returns | | --- | --- | --- | | `getPage(locale, slug)` | `GET /api/v1/pages/by-slug` | `Page` | | `listPages(locale, limit = 20)` | `GET /api/v1/pages` | `PageSummary[]` | | `listNews(locale, limit = 20)` | `GET /api/v1/news` | `NewsItem[]` | | `getNews(locale, slug)` | `GET /api/v1/news/by-slug` | `NewsItem` | | `listContent(type, query)` | `GET /api/v1/content/{type}` | `{ data: T[], metadata }` | | `getContent(type, id, locale?)` | `GET /api/v1/content/{type}/{id}` | `T` | | `memberLogin`, `memberRegister`, `memberRefresh`, `memberLogout`, `memberDevices`, `memberMe` | `/api/v1/mobile/member/*` | Token pairs, devices, profile | Every method takes an optional last argument `{ signal }` for cancellation. The `query` for `listContent` accepts `limit`, `offset`, `sort`, `search`, `locale` and `filter`. ## Examples ```ts const page = await hydro.getPage('en', 'about'); const latest = await hydro.listNews('en', 5); type Faq = { id: string; question: string; answer: string }; const faq = await hydro.listContent('faq', { limit: 20, search: 'refund' }); const one = await hydro.getContent('faq', faq.data[0].id); ``` ## Error handling Failures throw `HydroGoError` with `status`, `code`, `message` and `requestId`. Quote the request id when you report a problem. ```ts import { HydroGoError } from '@hydrogo/client'; try { await hydro.getPage('en', 'missing'); } catch (err) { if (err instanceof HydroGoError && err.status === 404) { // render your 404 } else if (err instanceof HydroGoError) { console.error(err.code, err.message, err.requestId); } else { throw err; } } ``` - Requests time out after 15 seconds by default. - Response bodies are size-capped to protect your server from runaway responses. - Member methods never send browser cookies (`credentials: 'omit'`). ## Generating a full SDK The client is hand-written for the public surface. For admin tooling and every OpenAPI type, generate an SDK: ```bash hydro sdk generate --lang typescript --out ./sdk hydro sdk generate --openapi docs/api/public-content-v1.openapi.json --lang all --out ./sdk ``` ## Without the client Plain `fetch` works everywhere. Unwrap `data` yourself: ```ts const res = await fetch('https://cms.example.com/api/v1/news?locale=en&limit=5'); if (!res.ok) throw new Error((await res.json()).request_id); const { data } = await res.json(); ``` --- # SvelteKit starter *Section: Frontends* *A small server-rendered site on the HydroGo API: the recommended shape for a real frontend.* `examples/sveltekit-starter` in the repository is a minimal, intentionally unstyled SvelteKit app that renders on the server with `adapter-node`. Search engines and link-preview bots receive complete HTML, and a publish in the admin is live on the next request. It contains no build step for content. ## Run it ```bash cd examples/sveltekit-starter cp .env.example .env # point HYDRO_API_ORIGIN at your HydroGo npm install npm run dev # http://localhost:5173 npm run build && npm start # production server on :3000 ``` | Variable | Meaning | | --- | --- | | `HYDRO_API_ORIGIN` | Where HydroGo runs. Server-side only, never sent to the browser. | | `PUBLIC_SITE_ORIGIN` | Public URL of this frontend, used for canonical links and the sitemap. | | `HYDRO_LOCALE` | Locale requested from the API (default `en`). | ## What is inside | Route | Does | | --- | --- | | `/` | Latest news with a short shared cache. | | `/news`, `/news/[slug]` | News list and article. | | `/[slug]` | A page by slug. | | `/sitemap.xml` | Generated from `listPages` and `listNews`. | | `src/lib/server/hydro.ts` | The shared client plus `toHttpError`, which turns API failures into proper HTTP statuses. | ```ts // +page.server.ts // src/routes/news/[slug]/+page.server.ts import { hydro, locale, siteOrigin, toHttpError } from '$lib/server/hydro'; export async function load({ params }) { try { const item = await hydro.getNews(locale, params.slug); return { item, canonical: `${siteOrigin}/news/${item.slug}` }; } catch (err) { toHttpError(err); } } ``` ```ts // hydro.ts // src/lib/server/hydro.ts export function toHttpError(err: unknown): never { if (err instanceof HydroGoError) { if (err.status === 404) error(404, 'Not found'); error(502, `Content API error${err.requestId ? ` (request ${err.requestId})` : ''}`); } error(502, 'Content API unreachable'); } ``` A 404 from HydroGo becomes a real 404 for visitors and crawlers; other failures become a 502 that carries the request id. > [!TIP] This is a starting point, not a theme. Replace the markup and keep the `+page.server.ts` loaders: they hold the data-access logic. ## Using another framework Nothing here is specific to SvelteKit. In Next.js, Nuxt, Astro, Remix or Express, fetch from server-side code on each request (or with a short revalidate window), render HTML and set cache headers as described in [Caching and webhooks](https://hydrogo.dev/docs#frontends/caching-and-webhooks). ```ts // Next.js (app router): revalidate every 30 seconds export default async function Page({ params }: { params: { slug: string } }) { const res = await fetch( `${process.env.HYDRO_API_ORIGIN}/api/v1/pages/by-slug?locale=en&slug=${params.slug}`, { next: { revalidate: 30 } }, ); const { data } = await res.json(); return
; } ``` --- # SEO *Section: Frontends* *Canonical URLs, Open Graph, JSON-LD and sitemaps for a headless site.* HydroGo stores SEO fields and generates helpers, but your frontend emits the HTML. Because the starter renders on the server, crawlers see everything on first response. ## Canonical, title and description Pages and news carry `title`; news also has `seo_title` and `seo_description`. Prefer the SEO fields when present and always emit a canonical link based on your **public** origin, not the CMS origin. ```svelte {data.item.seo_title ?? data.item.title} ``` ## Open Graph and social images ```svelte ``` `GET /og/{locale}/{slug}` generates an Open Graph image for pages and articles. Configure fonts with `seo.fonts_dir` and the cache with `seo.og_cache_dir`. ## JSON-LD structured data ```ts const jsonLd = { '@context': 'https://schema.org', '@type': 'NewsArticle', headline: item.title, datePublished: item.created_at, dateModified: item.updated_at, mainEntityOfPage: canonical, author: item.author_name ? { '@type': 'Person', name: item.author_name } : undefined, }; // ``` ## Sitemaps - HydroGo serves `/sitemap.xml`, `/sitemap-index.xml`, `/sitemap-pages.xml` and `/sitemap-articles.xml`, built from published content. - If your public site lives on another origin, generate the sitemap in your frontend from `listPages` and `listNews`, as the starter does, so the URLs match your routes. - Use `updated_at` for `` and send `cache-control: public, max-age=0, s-maxage=300`. ## Redirects and 404s Fetch `GET /api/v1/site/redirects` at start-up or on a short cache and apply them in your frontend or edge. In the admin, the **Not found** tab lists URLs visitors requested that do not exist and converts them into redirects. > [!WARNING] Do not index preview routes or staging copies. Send `X-Robots-Tag: noindex` for them and keep `security` headers strict on the CMS origin. --- # Caching and webhooks *Section: Frontends* *Keep the API fast with shared caching, and notify other systems when content changes.* ## Caching strategy Zero build on publish does not mean zero caching. Put a short cache between visitors and HydroGo and let freshness be a number you choose. | Layer | Setting | Effect | | --- | --- | --- | | Your frontend response | `cache-control: public, max-age=0, s-maxage=30, stale-while-revalidate=60` | A CDN serves a response for 30 s, then refreshes in the background. Edits appear within about half a minute. | | Sitemap | `s-maxage=300` | Crawlers rarely need fresher data. | | Media | Long cache lifetime at your CDN | Media objects are immutable: a replaced file gets a new id, so old URLs stay valid. | | Previews and admin | `private, no-store` | Never cache. | ```ts // SvelteKit load function export async function load({ setHeaders }) { const news = await hydro.listNews('en', 10); setHeaders({ 'cache-control': 'public, max-age=0, s-maxage=30, stale-while-revalidate=60' }); return { news }; } ``` > [!TIP] Need instant updates for a specific page? Skip the cache for it, or purge the CDN from a webhook (below). ## Webhooks HydroGo can POST signed JSON events to your systems when content changes, for example to purge a CDN, trigger a static rebuild or notify a chat room. ```yaml // config.yaml webhooks: - id: frontend url: https://ci.example.com/hooks/hydrogo topics: [page.published, news.published, news.retracted] ``` ```bash # the secret never lives in YAML; it is read from the environment HYDRO_WEBHOOK_SECRET=change-me # shortcut for a single endpoint without any YAML HYDRO_WEBHOOK_URL=https://ci.example.com/hooks/hydrogo HYDRO_WEBHOOK_TOPICS=page.published,news.published,news.retracted ``` | Topic | Fires when | | --- | --- | | `page.created`, `page.updated`, `page.published` | A page changes or is published. | | `news.created`, `news.updated`, `news.scheduled`, `news.published`, `news.retracted` | The news lifecycle. | | `user.invited` | A staff invitation is created (contains personal data and is only sent to endpoints with `allow_pii: true`). | | `order.created`, `tenant.provisioned` | Commerce and tenancy events. | List the exact catalog your binary supports with `hydro events catalog`. ### Event payload ```json { "id": "evt_01", "topic": "news.published", "aggregate_id": "art_42", "payload_version": 1, "data": { } } ``` ### Verify the signature Each request carries `X-Hydro-Event`, `X-Hydro-Timestamp`, `X-Hydro-Signature` and an `Idempotency-Key` (the event id). The signature is `sha256=` followed by the hex HMAC-SHA256 of `.` using your secret. Reject requests with an old timestamp and de-duplicate by `Idempotency-Key`; deliveries are retried on failure. ```ts import { createHmac, timingSafeEqual } from 'node:crypto'; export function verify(rawBody: string, headers: Headers, secret: string): boolean { const ts = headers.get('x-hydro-timestamp') ?? ''; const sig = headers.get('x-hydro-signature') ?? ''; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'); const a = Buffer.from(sig); const b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b); } ``` ## Static frontends If you deliberately use a static site generator, point a webhook at your CI provider's build trigger and subscribe to `page.published`, `news.published` and `news.retracted`. This is an option for your frontend, not a requirement of HydroGo: server rendering with a short cache avoids rebuilds entirely. > [!WARNING] Webhook URLs must be plain `http` or `https` URLs without embedded credentials, and redirects are validated again. Use HTTPS and always verify the signature. ================================================================================ # SECTION: MULTI-LANGUAGE ================================================================================ --- # Content locales *Section: Multi-language* *Publish your site in several languages: the i18n configuration, per-locale pages and news, localized content type fields and the ?locale= parameter.* HydroGo has two independent language concepts. **Content locales** (this page) are the languages of your site's content. The **admin interface language** is what editors see in the panel; see [Admin interface language](https://hydrogo.dev/docs#multi-language/admin-language). ## Enable locales ```yaml // config.yaml i18n: enabled: true default_locale: en locales: [en, tr, de] ``` ```bash HYDRO_I18N_ENABLED=true HYDRO_DEFAULT_LOCALE=en HYDRO_LOCALES=en,tr,de hydro serve --config config.yaml ``` - `default_locale` must not be empty and must be one of `locales`. - `locales` holds 1 to 64 codes such as `en`, `tr`, `pt-BR` or `zh-Hans`. - Restart the server after changing locales. ## Pages and news Pages and news articles are **per-locale documents**: each has a `locale`, and the slug is unique within that locale. Create the English `about` page and a Turkish `hakkimizda` page as two documents (the page editor also offers a **Translate** action to start from an existing page). ```bash curl 'https://cms.example.com/api/v1/pages/by-slug?locale=en&slug=about' curl 'https://cms.example.com/api/v1/pages/by-slug?locale=tr&slug=hakkimizda' curl 'https://cms.example.com/api/v1/news?locale=tr&limit=10' ``` Always pass `locale` to the pages and news endpoints. They do not fall back to another locale's document. ## Localized fields in content types In a content type, mark the fields whose text differs per language with `localized: true`. Storage keeps one value per locale. ```yaml // content-types/team.yaml id: team label: Team revisions: true # required when any field is localized public: true fields: - {id: name, label: Name, kind: text, required: true, list: true} - {id: role, label: Role, kind: text, list: true, localized: true} - {id: bio, label: Bio, kind: textarea, localized: true} - {id: photo, label: Photo, kind: media, localized: true} ``` | Rule | Detail | | --- | --- | | Allowed kinds | `text`, `textarea`, `richtext`, `media`. | | Not combinable with | `unique`, `searchable`, `filterable`. | | Requires | `revisions: true` on the type. | | Default locale | The record itself holds the default locale's value; other locales are stored beside it. | | Activation | Only when `i18n.enabled` is true **and** more than one locale is configured. Otherwise the flag changes nothing. | ### In the admin Records of types with localized fields show **locale tabs**. Edit the default locale first, then switch tabs to fill the others. A **copy from** action seeds a locale from another one. ### In the API ```bash # default locale curl 'https://cms.example.com/api/v1/content/team' # Turkish values; any missing field falls back to the default locale per field curl 'https://cms.example.com/api/v1/content/team?locale=tr' ``` With `?locale=` the response is flagged with `metadata.locale` and each record gets a `locale` field. **Fallback happens field by field**: if only `role` is translated, `bio` still comes back in the default language. An unknown locale answers `400`. > [!NOTE] Search, filters and sorting always use the default locale's values, even when `?locale=` asks for another. ## From your frontend ```ts const locale = params.lang ?? 'en'; const page = await hydro.getPage(locale, params.slug); const news = await hydro.listNews(locale, 10); const team = await hydro.listContent('team', { locale }); ``` > [!TIP] Link translations to each other in your frontend by convention (for example the same page id in your own mapping, or a shared tag). HydroGo does not enforce that a translation exists for every locale. ## Site-level text Navigation menus and translation strings are stored per locale too: **Site > Navigation** and **Site > Translations** in the admin, and `GET /api/v1/site/navigation/{locale}` in the API. --- # Admin interface language *Section: Multi-language* *Each person can pick the language of the admin panel; an administrator sets the project default. English and Turkish are included.* The admin panel is localized. This affects only the **interface** (menus, buttons, messages), never the content you edit or what the public API returns. Supported locales today: `en` (English) and `tr` (Türkçe). ## Pick your own language 1. Open **My Account** from the user menu. 2. Find the **Language** section. 3. Choose **English**, **Türkçe**, or **Use the project default**. The change applies immediately. It is saved to your account and remembered on this browser. > [!NOTE] The account setting is saved with `PUT /api/v1/admin/account/locale`. ## Set the project default 1. Sign in as an administrator and open **Site settings**. 2. On the **General** tab, find **Admin language (project default)**. 3. Choose **English**, **Türkçe** or **Automatic (browser language)**, then save. The default is used for everyone who has not chosen their own language in My Account. The value is stored in the site setting `admin.default_locale`. | Where | Label | Who | Effect | | --- | --- | --- | --- | | My Account, Language | My language | Each user | Overrides the project default for this account. | | Site settings, General | Admin language (project default) | Administrators | Applies to every user without a personal choice. | ## How the language is chosen 1. The signed-in user's own choice in My Account. 2. Otherwise the project default from Site settings. 3. If that is **Automatic**, the language of the browser (`en` or `tr`), and English when the browser language is not supported. ## For integrators | Endpoint | Purpose | | --- | --- | | `GET /api/v1/admin/i18n/{locale}` | Returns the translation catalogue of the admin for `en` or `tr`. | | `PUT /api/v1/admin/account/locale` | Saves the signed-in user's language. An empty value means "use the project default". | | `GET /api/v1/admin/schema` | Includes `locales.admin_default`, the project default chosen in Site settings (empty until set). | > [!TIP] Admin language and content locales are separate on purpose: an editor can work in a Turkish interface while writing the English version of a page. > [!WARNING] Adding a content locale (for example `de`) does not add a German admin interface. The panel's languages ship with HydroGo. ================================================================================ # SECTION: AI & MCP AGENT GUIDE ================================================================================ --- # AI Control Plane & MCP *Section: AI & MCP Agent Guide* *How HydroGo's native Model Context Protocol (MCP) server enables AI coding agents to inspect, model, and mutate schemas at runtime.* HydroGo includes a built-in **Model Context Protocol (MCP)** server. This allows AI development agents—such as Cursor, Claude Code, and Claude Desktop—to introspect active collections, generate schema definitions, and mutate records in real time without restarting the Go server. Every action executed through the MCP interface respects tenant isolation boundaries, runs validations against content schemas, and writes cryptographic audit trails. ## Why Native MCP? - **Zero Server Reboots:** AI agents add new content types or modify fields dynamically; HydroGo registers the routes and database migrations on the fly. - **Type-Safe Tool Contracts:** Agents communicate through structured JSON-RPC over stdio or HTTP/SSE, eliminating hallucinations and invalid SQL queries. - **Cryptographic Ledger:** Every mutation is cryptographically hashed with SHA-256 in `hydro_audit_logs`, providing a tamper-evident audit history of all agent modifications. > [!TIP] You can inspect the complete MCP setup guide in the official repository at `docs/guides/mcp-agent-guide.md`. --- # Connecting AI Agents *Section: AI & MCP Agent Guide* *Step-by-step instructions to configure Cursor, Claude Desktop, and Claude Code with HydroGo MCP in under 30 seconds.* ## 1. Generate MCP Config Run the following command in your HydroGo project root: ```bash hydro mcp config ``` This prints the formatted JSON configuration required by AI clients: ```json { "mcpServers": { "hydrogo": { "command": "/path/to/bin/hydro", "args": ["mcp"] } } } ``` ## 2. Client Configuration ### Cursor Add the configuration to `.cursor/mcp.json` in your project root or configure in **Cursor Settings > Features > MCP**: - **Name:** `hydrogo` - **Type:** `command` - **Command:** `hydro mcp` ### Claude Desktop Add the `hydrogo` server block to your `claude_desktop_config.json`: - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` - **Linux:** `~/.config/Claude/claude_desktop_config.json` ### Claude Code (CLI) Connect HydroGo directly from the command line: ```bash claude mcp add hydrogo -- hydro mcp ``` --- # Available Tools & Audit *Section: AI & MCP Agent Guide* *The 4 core tools exposed by HydroGo's MCP server and the SHA-256 cryptographic audit chain.* ## Core MCP Tools | Tool Name | Alias | Description | | --- | --- | --- | | `list_content_types` | `schema_list` | Introspects all active collection schemas, field validations, and declarative access rules. | | `define_content_type` | `schema_apply` | Dynamically creates or updates a YAML collection schema at runtime without server restart. | | `query_collection` | `content_query` | Queries records in any collection with filtering, sorting, pagination, and full-text search. | | `mutate_record` | `content_mutate` | Creates, updates, or deletes records in dynamic collections with cryptographic audit logging. | ## Prompt Example: Dynamic Collection Scaffolding Give this prompt to Cursor or Claude Code: ```markdown Connect to HydroGo MCP. Create a new collection schema for 'vehicles' with fields: - brand (text, required) - model (text, required) - daily_rate (number, required) - available (boolean) Then insert 2 sample rental cars. ``` ## What Happens Under the Hood 1. The AI agent invokes `define_content_type` with the structured schema JSON. 2. HydroGo writes `content-types/vehicles.yaml`, runs an atomic zero-downtime database migration across SQLite, Postgres, or MySQL, and registers `/api/v1/content/vehicles`. 3. The agent calls `mutate_record` with `operation: "create"` to seed the initial rows. 4. The new collection and records appear instantly inside the Svelte 5 Admin Studio under `/admin/content/vehicles` with zero build step. ## Cryptographic Audit Trail All agent mutations are recorded into the `hydro_audit_logs` ledger. Each log entry contains the actor identity, timestamp, JSON diff payload, and a SHA-256 hash chained to the previous entry, ensuring an unalterable history of AI actions. > [!NOTE] To inspect agent mutations via CLI: `hydro audit list --limit 20 --json` ================================================================================ # SECTION: SECURITY ================================================================================ --- # Hardening checklist *Section: Security* *CSRF protection, secure cookies, HSTS, security headers, rate limiting and the other defences that are on by default or one setting away.* HydroGo is meant to be safe with defaults and tight with a few production settings. This page is the checklist to walk through before you go live. ## Production checklist | Setting | Value | Why | | --- | --- | --- | | `http.public_origin` | `https://cms.example.com` | Canonical origin used in links, cookies and previews. | | `security.secure_cookies` | `true` | Cookies are only sent over HTTPS. | | `security.enable_hsts` | `true` | Browsers refuse to downgrade to HTTP. Enable once HTTPS works everywhere. | | `security.mfa_master_key` | 16, 24 or 32 random bytes | Encrypts stored TOTP secrets. | | `security.mfa_required_roles` | `[admin]` | Force MFA for powerful roles. Enrol accounts first. | | `security.preview_signing_key` | 32+ random bytes | Signs draft preview links. | | `security.public_api_origins` | Your frontend origins | Allow cross-origin browser reads of published content only from these origins. | | `database.dsn` | From environment | Keep credentials out of files you commit. | ```bash # generate a suitable 32 byte secret openssl rand -hex 16 # 32 hex characters = 32 bytes of text openssl rand -base64 32 ``` ## CSRF protection Browser sessions are protected against cross-site request forgery. The server sets a `hydro_csrf` cookie and requires its value in the `X-CSRF-Token` header on every unsafe request (`POST`, `PUT`, `PATCH`, `DELETE`) that authenticates with the session cookie. Fetch the current token from `GET /csrf-token` or `GET /api/v1/admin/me`. Requests authenticated with a Bearer token or API key do not use cookies and are not subject to CSRF. ## Cookies - `hydro_session` is HTTP-only and `SameSite`, and gets the `Secure` flag when `secure_cookies` is on. - Sessions last up to 7 days and expire after 24 hours of inactivity by default. Users can sign out other sessions from My Account. - Password reset and invitation links are single use and expire (`password_reset_ttl`, `invitation_ttl`). ## Response headers `headers_preset: strict` (the default) sends a restrictive `Content-Security-Policy`, `X-Frame-Options`, `Referrer-Policy` and `Permissions-Policy`. With `enable_hsts` it also sends `Strict-Transport-Security`. CSP violations can be reported to `POST /csp-report`. ## Rate limiting and abuse protection - Authentication and sensitive endpoints are rate limited per client (see the table below). Limited requests answer `429` with a `Retry-After` header. - Repeated failed sign-ins are also slowed down by a login tarpit. - Limits are kept in process by default. Set `redis.url` so every instance shares the same counters. - The **anomaly filter** (`enable_anomaly_filter`) rejects obviously malicious requests; list endpoints it must not touch in `anomaly_bypass_paths`. - Administrators can ban an IP or CIDR range in the admin or with `hydro ban 203.0.113.0/24 --for 24h`. | Endpoint | Built-in limit | | --- | --- | | Staff login and MFA code | 5 per minute | | Password reset request | 3 per hour | | E-mail verification | 3 per hour | | Public form submission | 3 per hour | | Member sign-in and registration | 10 per minute | | Admin backup download | 3 per minute | > [!WARNING] Behind a proxy or CDN, make sure client IPs reach HydroGo correctly, otherwise rate limits and bans apply to the proxy instead of visitors. For GeoIP headers, list the proxies you trust in `geoip.trusted_proxy_cidrs`. ## Uploads and media - Uploads are validated and re-encoded (images become WebP derivatives), and the library refuses new files when free disk space falls below `media.min_free_bytes`. - `media.hotlink_protection` with `allowed_referer_hosts` keeps other sites from embedding your files. ## Secrets - Never commit real values for DSNs, SMTP passwords, S3 keys or signing keys. Use `HYDRO_*` environment variables or your platform's secret store. - Webhook secrets are never read from YAML; set `HYDRO_WEBHOOK_SECRET`. - Personal tokens and API keys are shown once at creation and stored hashed. --- # Accounts, MFA and API keys *Section: Security* *Roles and permissions, TOTP multi-factor authentication, personal tokens and API keys.* ## Roles and permissions | Role | Scope | | --- | --- | | `admin` | Full access, including user management and system tools. | | `editor` | Content work; write access depends on granted permissions. | | `viewer` | Read-only access to the admin. | | Permission | Grants | | --- | --- | | `content.write` | Create, edit, publish and delete pages, news and content records. | | `media.upload` | Manage media files. | | `site.manage` | Site settings, maintenance mode, redirects. | | `users.manage` | Invite users, change roles and permissions. | | `telemetry.read` | Runtime and diagnostic views. | Grant the minimum a person needs. Every endpoint checks permissions on the server regardless of what the admin UI shows. ## Multi-factor authentication (TOTP) 1. Install an authenticator app (any TOTP-compatible one). 2. In **My Account**, start MFA enrolment and scan the QR code or enter the secret. 3. Confirm with a six-digit code. MFA is now required at every sign-in. 4. Store the recovery codes shown at enrolment somewhere safe. To make MFA mandatory for a role, enrol its users first and then set `security.mfa_required_roles: [admin]`. Stored secrets are encrypted with `security.mfa_master_key`; keep that key stable, because losing or changing it invalidates enrolments. > [!WARNING] Do not turn on `mfa_required_roles` before every affected account has enrolled. Accounts without a second factor cannot sign in afterwards. ## Personal tokens and API keys | | Personal token | API key | | --- | --- | --- | | Created by | Any signed-in user, from a browser session | Administrators | | Acts as | Its owner | A read-only machine identity | | Scopes | Owner's full access | `read` or `preview` | | Header | `Authorization: Bearer hyd_…` | `X-API-Key: hyd_…` | | Expiry | Optional, up to 3650 days | Optional, up to 3650 days | | Revoke | My Account, or `DELETE /api/v1/admin/account/tokens/{id}` | Users and roles, or `DELETE /api/v1/admin/api-keys/{id}` | > [!TIP] Create one API key per consumer (staging SSR, production SSR, CI) and set an expiry. Revoking one never affects the others, and the audit trail records issue and revoke events. ## Social sign-in Staff can sign in with Google, GitHub or Apple, but only into an **existing, active** admin or editor account that matches the verified provider identity: social sign-in never creates staff accounts. Members (site visitors) can also use social sign-in to register themselves. Provider credentials are supplied through `HYDRO_OAUTH_*` and `HYDRO_MEMBER_OAUTH_*` environment variables. ## Onboarding and offboarding - Invite staff by e-mail instead of sharing passwords. - To offboard, disable the user; sessions end and history stays intact. --- # Audit trail *Section: Security* *Who changed what and when, plus optional redacted request bodies for admin API calls.* HydroGo writes security-relevant and content-changing actions to an append-only **audit trail**: signing in, role changes, token and API key lifecycle, publishing, content edits and more. Open it in the admin under **Audit Log**, or read it from `GET /api/v1/admin/audit`. ## What an entry contains | Field | Meaning | | --- | --- | | `at` | When it happened (UTC). | | `actor`, `actor_id` | The user who did it, or `system` for background work. | | `action` | What happened, for example `auth.api_key.issued`. | | `resource`, `target` | The kind of thing changed and its id. | | `request_id` | The request id, to match logs and API error responses. | | `before_json`, `after_json` | Compact before and after details where the action records them (for example version numbers). | Filter the list by `actor`, `action`, `resource` or `target`, in the admin or the API: ```bash curl 'https://cms.example.com/api/v1/admin/audit?actor=editor@example.com&action=admin.request&limit=50' \ -H 'Authorization: Bearer hyd_…' ``` ## Recording request bodies By default an entry says *that* something changed. To also see *what the client sent*, turn on request-body logging. It is off by default. **config.yaml**: ```yaml audit: log_request_bodies: true ``` **Environment**: ```bash HYDRO_AUDIT_LOG_REQUEST_BODIES=true ``` With it enabled, every state-changing admin API request (`POST`, `PUT`, `PATCH`, `DELETE` under `/api/v1/admin/`) by a signed-in user adds one `admin.request` entry containing the method, path, status code, client IP, user agent, redacted query string and the redacted JSON body. | Safeguard | Behaviour | | --- | --- | | Secrets are redacted | Any JSON key whose name contains `password`, `secret`, `token`, `authorization`, `cookie`, `credential`, `private`, `signature`, `api_key`, `totp`, `mfa`, `recovery` or `otp` is replaced by `[redacted]`, at any depth. | | Bodies are capped | Details are kept under a few KiB (about 3.8 KiB). Long strings and deep structures are shortened; if it still does not fit, the body is dropped and `body_truncated` is set. | | Oversized requests | A body larger than 64 KiB is not captured at all; the entry says so. | | Only JSON | File uploads and non-JSON requests are skipped. | | The request is untouched | The handler still receives the complete body; capture only reads a bounded copy. | ```json // details of an admin.request entry { "method": "PUT", "path": "/api/v1/admin/pages/pg_01", "status": 200, "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 …", "body": { "title": "About us", "password": "[redacted]" } } ``` > [!WARNING] Even redacted, bodies can contain personal data or unpublished text. Turn this on when you need accountability (regulated teams, agencies handing over a site) and restrict who may open the Audit Log. ## Retention Audit entries live in your database. Include them in backups, and prune according to your own retention policy. --- # Reporting vulnerabilities *Section: Security* *Supported versions and how to report a security issue privately.* ## Supported versions HydroGo is under active development. Security fixes are applied to the latest `main` branch, the latest release and the release before it. Older releases and development snapshots are not a security-support promise. See [Upgrading](https://hydrogo.dev/docs#upgrading/upgrade-steps) to move to a fixed release. ## How to report Please **do not open a public issue** for an undisclosed vulnerability. Report it through a private GitHub Security Advisory for the repository. If private reporting is unavailable, contact the maintainers through a private channel and put “HydroGo security report” in the subject. Please include: - the affected commit, release or component, - a clear description of the impact and prerequisites, - reproduction steps or a minimal proof of concept, - any suggested mitigation, and whether it is already known to be exploited. > [!WARNING] Do not include real customer data, production credentials, signing keys or session tokens in a report. Reports are acknowledged, investigated, and disclosed in coordination after a fix or mitigation is available. Mention your profile, database, storage provider and deployment topology when reporting an operational issue. ================================================================================ # SECTION: DEPLOYMENT ================================================================================ --- # VPS with systemd and a proxy *Section: Deployment* *Run HydroGo on any Linux server as a systemd service behind Caddy or nginx for TLS.* HydroGo is one static binary plus a config file and a data directory. On a VPS the usual shape is: `hydro serve` bound to localhost, managed by systemd, with Caddy or nginx terminating TLS in front. ## 1. Install and create the project ```bash curl -fsSL https://github.com/hydrohq/hydrogo/releases/latest/download/install.sh | sh sudo useradd --system --home /srv/mysite --shell /usr/sbin/nologin hydro hydro new mysite --preset corporate --admin-email you@example.com sudo mv mysite /srv/mysite && sudo chown -R hydro:hydro /srv/mysite ``` > [!NOTE] `hydro new` refuses to write into a folder that already exists, so scaffold first and move the result into place (or build your project on your laptop and copy it). ## 2. Production configuration Set these in `config.yaml` or as `HYDRO_*` variables (see [Environment variables](https://hydrogo.dev/docs#configuration/environment-variables)): ```yaml // config.yaml profile: standard http: address: "127.0.0.1:8080" public_origin: "https://cms.example.com" behind_proxy: true security: secure_cookies: true enable_hsts: true ``` > [!NOTE] Enrol administrators in MFA before you enable `security.mfa_required_roles`. ## 3. systemd unit ```ini // hydrogo.service # /etc/systemd/system/hydrogo.service [Unit] Description=HydroGo CMS After=network-online.target Wants=network-online.target [Service] User=hydro WorkingDirectory=/srv/mysite EnvironmentFile=-/etc/hydrogo/env ExecStartPre=/usr/local/bin/hydro migrate --config config.yaml ExecStart=/usr/local/bin/hydro serve --config config.yaml Restart=on-failure RestartSec=3 NoNewPrivileges=true ProtectSystem=full ReadWritePaths=/srv/mysite [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now hydrogo sudo journalctl -u hydrogo -f ``` > [!TIP] Put secrets (DSN, keys) in `/etc/hydrogo/env` with mode `0600`, owned by root, instead of in the unit file or `config.yaml`. ## 4. TLS proxy **Caddy**: ```caddyfile // Caddyfile cms.example.com { encode zstd gzip header { Strict-Transport-Security "max-age=63072000; includeSubDomains" X-Content-Type-Options "nosniff" -Server } reverse_proxy 127.0.0.1:8080 { header_up X-Real-IP {remote_host} header_up X-Forwarded-Proto https } } ``` **nginx**: ```nginx // nginx.conf server { listen 443 ssl http2; server_name cms.example.com; ssl_certificate /etc/letsencrypt/live/cms.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/cms.example.com/privkey.pem; client_max_body_size 64m; # media uploads location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } } server { listen 80; server_name cms.example.com; return 301 https://$host$request_uri; } ``` The repository contains complete, hardened examples in `deploy/caddy/Caddyfile` and `deploy/nginx/nginx.conf`. ## Built-in TLS Without a proxy, HydroGo can obtain certificates itself: set `http.auto_tls: true` and `http.domain: cms.example.com` (ports 80 and 443 must be reachable). It cannot be combined with `http.behind_proxy`. ## Verify ```bash curl -fsS https://cms.example.com/readyz curl -fsS https://cms.example.com/api/v1/news?locale=en\&limit=1 hydro doctor --config /srv/mysite/config.yaml ``` --- # Docker *Section: Deployment* *Run the published image, or the production Compose stack with Caddy, PostgreSQL and Redis.* ## Run the image ```bash docker run -p 8080:8080 -v hydro-data:/app/data ghcr.io/hydrohq/hydrogo:latest ``` Images are published to `ghcr.io/hydrohq/hydrogo` for linux/amd64 and arm64. Tags follow releases (`v0.2.0`) and `latest`; pre-release tags (with a hyphen, like `v0.2.0-rc.1`) never move `latest`. Pin a version in production. Mount a project folder at `/app` (it holds `config.yaml`, `content-types/` and `data/`), or just a config file at `/app/config.yaml`, and keep the data on a volume: ```bash docker run -d --name hydrogo -p 8080:8080 \ -v "$(pwd)":/app \ -e HYDRO_PUBLIC_ORIGIN=https://cms.example.com \ ghcr.io/hydrohq/hydrogo:v0.2.0 ``` > [!NOTE] The Dockerfile in `deploy/` builds a non-root image whose working directory is `/var/lib/hydrogo`. Whichever image you use, the rule is the same: mount a persistent volume where your `database.dsn` and `media.dir` point. ## Production Compose stack `deploy/docker-compose.prod.yml` is a complete example: Caddy for TLS, HydroGo, PostgreSQL and Redis on a private network, with credentials supplied as Docker secrets. The entrypoint reads secret files and exports the matching `HYDRO_*` variables (database DSN, Redis URL, MFA key, S3 and SMTP credentials). ```bash cd deploy export HYDRO_PUBLIC_HOST=cms.example.com docker compose -f docker-compose.prod.yml up -d --build docker compose -f docker-compose.prod.yml ps ``` - The app container runs as a non-root user (`10001`). - Its health check calls `GET /readyz`; Caddy waits for it before accepting traffic. - Run `hydro migrate` as an explicit operator step before starting a new version; runtime start-up does not apply schema changes on its own in this stack. - Roll back by redeploying the previous image tag, never by deleting tables. ## Docker health check The binary has a built-in probe usable in `HEALTHCHECK` lines without curl: `hydro healthcheck` requests `http://127.0.0.1:8080/healthz` (override with `HYDRO_HEALTHCHECK_URL`). ```text // Dockerfile HEALTHCHECK --interval=15s --timeout=4s CMD ["hydro", "healthcheck"] ``` --- # Shared hosting (CGI and FastCGI) *Section: Deployment* *Deploy on Apache-style shared hosting where you cannot run a long-lived process.* Many shared plans cannot keep a daemon running. HydroGo has two compatibility modes for them, at the cost of some performance. Use a VPS if you can; use these when you cannot. ## Build for the host Cross-compile a static, CGO-free `linux/amd64` binary (shared hosts have no C toolchain): ```bash make build-hosting # Linux and macOS # Windows: scripts/build-hosting.ps1 ``` ## CGI mode CGI starts a fresh process for each request, so there are no resident workers. The wrapper script is what Apache executes; keep the binary and `config.yaml` **outside the document root** (for example a sibling `private/` folder). ```bash // hydro.cgi #!/bin/sh exec /path/to/private/hydro serve --cgi --config /path/to/private/config.yaml ``` - Upload `hydro.cgi` through SFTP or the panel's upload function, not by pasting into an in-panel text editor. Files created in a panel can get different ownership and fail `suexec` checks with `Permission denied`. - Add `DirectoryIndex hydro.cgi` to `.htaccess`. Without it, a request for `/` may return 403 once any placeholder `index.html` is gone. - Apache serves existing files directly and sends everything else to HydroGo. - Scheduled news jobs run in bounded batches during public requests. > [!WARNING] Under CGI every request opens its own database connection. SQLite's single-writer lock is risky under concurrency, so prefer MySQL or PostgreSQL when your plan offers it, and keep SQLite for low-traffic, single-editor sites. ### Running migrate without SSH If you have no shell, a one-shot CGI script can run `hydro migrate`. Protect it with a long random token, return 404 on mismatch, and delete it right after use. ```bash // migrate.cgi (delete after use) #!/bin/sh TOKEN="" case "$QUERY_STRING" in token=$TOKEN) echo "Content-Type: text/plain"; echo "" /path/to/private/hydro migrate --config /path/to/private/config.yaml 2>&1 ;; *) echo "Status: 404 Not Found"; echo "Content-Type: text/plain"; echo ""; echo "not found" ;; esac ``` ## FastCGI mode FastCGI is a long-running responder: start it under the provider's process manager and point Apache's FastCGI integration at the loopback address. The port is not bound to public interfaces. ```bash hydro serve --fastcgi --fastcgi-address 127.0.0.1:9001 ``` HydroGo keeps its bounded background workers in this mode and drains requests on shutdown. The full guide, including `.htaccess` examples and the static page cache that lets Apache answer eligible public pages without starting CGI, is in `deploy/hosting/README.md`. --- # Databases, Redis and media storage *Section: Deployment* *Choose SQLite, PostgreSQL or MySQL, add Redis for several instances, and move media to S3.* ## Databases | Dialect | Use when | DSN example | | --- | --- | --- | | `sqlite` (default) | One instance, small to medium traffic, simple backups. | `data/hydro.db` | | `postgres` | More traffic or several instances. | `postgres://hydro_app:secret@db.internal:5432/hydro?sslmode=verify-full` | | `mysql` (and MariaDB) | You already run MySQL, or your host provides it. | `hydro:secret@tcp(db.internal:3306)/hydro` | ```bash HYDRO_DATABASE_DIALECT=postgres HYDRO_DATABASE_DSN=postgres://hydro_app:REPLACE_ME@db.internal:5432/hydro?sslmode=verify-full hydro migrate --config config.yaml ``` Every migration ships SQL for all three dialects, so you can change database later by exporting and importing content (see `hydro portable export` and `hydro portable import`) into a freshly migrated database. ### SQLite notes - Keep the database on local disk, not a network share. - Only HydroGo should write to the file; set `database.sqlite_external_write_watch: true` if other tools might. - Back it up with `hydro backup sqlite`, not by copying a live file. ## Redis or Valkey Optional for one instance, recommended for several. With `redis.url` set, rate limits and caches are shared, so counters are correct no matter which instance answers. ```bash HYDRO_REDIS_URL=rediss://:REPLACE_ME@redis.internal:6379/0 HYDRO_REDIS_KEY_PREFIX=hydro: ``` ## Media on S3 By default media lives in `media.dir` on local disk. For multiple instances, containers without volumes, or large libraries, use an S3-compatible bucket (AWS S3, MinIO, Cloudflare R2, and similar) and, optionally, a CDN. ```bash HYDRO_MEDIA_STORAGE_DRIVER=s3 HYDRO_MEDIA_S3_ENDPOINT=s3.eu-central-1.amazonaws.com HYDRO_MEDIA_S3_BUCKET=example-media HYDRO_MEDIA_S3_REGION=eu-central-1 HYDRO_MEDIA_S3_ACCESS_KEY=REPLACE_ME HYDRO_MEDIA_S3_SECRET_KEY=REPLACE_ME HYDRO_MEDIA_CDN_BASE_URL=https://cdn.example.com # self-hosted stores often need path-style addressing HYDRO_MEDIA_S3_FORCE_PATH_STYLE=true ``` > [!NOTE] Moving existing local media to S3 means uploading the files to the bucket under the same keys. Test on a copy first. ## Sizing guidance | Setup | Profile | Database | Extras | | --- | --- | --- | --- | | Personal site, one editor | `compact` | SQLite | none | | Company site or blog | `standard` | SQLite or PostgreSQL | CDN for media | | High traffic or redundancy | `distributed` | PostgreSQL or MySQL | Redis, S3, several instances | --- # Backups and restore *Section: Deployment* *Back up the database and media, verify them, and practise a restore before you need one.* A backup you have never restored is a hope, not a backup. Schedule backups, copy them off the machine and rehearse the restore on a scratch copy. ## Back up ```bash # SQLite database plus media directory hydro backup sqlite --destination backups/ --media-dir data/media --config config.yaml # PostgreSQL hydro backup postgres --destination backups/ --config config.yaml ``` Run it from cron or a systemd timer: ```ini // hydrogo-backup.timer # /etc/systemd/system/hydrogo-backup.timer [Timer] OnCalendar=*-*-* 03:15:00 Persistent=true [Install] WantedBy=timers.target ``` ```bash # keep the 14 newest, preview first hydro backup prune --root backups/ --keep 14 --dry-run hydro backup prune --root backups/ --keep 14 --confirm ``` For MySQL or MariaDB use your own `mysqldump`. For PostgreSQL you can also use `pg_dump` directly. ## Scheduled cloud backups With S3 configured you can enable scheduled backups inside HydroGo (`backup.enabled`, `prefix`, `retention_days`, `hour_utc`). The admin's **Telemetry > Backups** tab lists and verifies signed backups; sign them with `HYDRO_BACKUP_SIGNING_SEED`. ## Verify and restore ```bash hydro restore verify --snapshot backups/hydro-2026-04-01.snapshot # unpack an archive into a NEW directory hydro restore unpack --archive backups/site.tar.gz --destination restore/ --public-key HEX ``` > [!WARNING] Restores are deliberately guarded. `hydro restore stage` and `hydro restore postgres` require explicit flags confirming that sessions were revoked, egress is quarantined and old writes are blocked, so a restore cannot silently replay old sessions or double-write. ## Portable export `hydro portable export` writes the content of your site to a portable document, and `hydro portable import --dry-run` validates it before `--confirm` replaces content. Use it to move between databases or environments. ```bash hydro portable export --path export.json --config config.yaml hydro portable import --path export.json --dry-run --config config.yaml ``` --- # Health, metrics and diagnostics *Section: Deployment* *Probes for load balancers, Prometheus metrics, the doctor command and error reporting.* ## Health endpoints | Endpoint | Meaning | Use for | | --- | --- | --- | | `GET /healthz` | The process is alive. | Container restart policies and uptime checks. | | `GET /readyz` | The process is ready to serve (dependencies reachable). | Load balancer and rolling-deploy gates. | | `hydro healthcheck` | CLI probe of `/healthz` (override with `HYDRO_HEALTHCHECK_URL`). | Docker `HEALTHCHECK`. | ## Metrics `GET /metrics` exposes Prometheus metrics. Restrict it at your proxy to your monitoring network. The admin's Telemetry area shows uptime, requests, memory, database and queue statistics, and the busiest paths. ```yaml # prometheus.yml scrape_configs: - job_name: hydrogo metrics_path: /metrics static_configs: - targets: ['cms.internal:8080'] ``` ## hydro doctor and validate ```bash hydro validate --config config.yaml # config and database sanity hydro doctor --config config.yaml # deeper install checks hydro doctor --json --config config.yaml hydro doctor --fix --config config.yaml # apply safe automatic fixes ``` ## Error reporting and tracing - Set `observability.sentry_dsn` (or `HYDRO_SENTRY_DSN`) to send errors to Sentry or GlitchTip. - On the `distributed` profile, OpenTelemetry export can be enabled with `HYDRO_OTEL_ENABLED`. - Every response carries `X-Request-ID`. Error bodies include it as `request_id`, and the audit trail stores it, so one id links a user report to logs. ## Background jobs Failed background jobs end up in a dead-letter queue. Inspect and manage it from the CLI: ```bash hydro jobs dead-letter list --limit 20 --json hydro jobs dead-letter requeue --id JOB_ID --confirm ``` ================================================================================ # SECTION: UPGRADING ================================================================================ --- # Versioning policy *Section: Upgrading* *How HydroGo numbers releases, what a version bump may break, and how database migrations are handled.* HydroGo follows [Semantic Versioning](https://semver.org). The version is injected at build time and shown by `hydro version`. | Release | May contain | | --- | --- | | Patch (`0.2.1`) | Bug fixes only. | | Minor (`0.3.0`) while the major is 0 | New features **and** changes to the API or config. Each such change is listed under "Changed" or "Removed" in the changelog with what to do. | | Major (`1.0.0` and later) | Breaking API changes only come with a new major version. | | Pre-release (`0.3.0-rc.1`) | Published as a GitHub pre-release; does not move the `latest` image tag. | ## Migrations - Database migrations are **forward-only** and never edited after release, so any 0.x database can be upgraded to the next release by running the new binary's `hydro migrate`. - Every migration ships SQL for SQLite, PostgreSQL and MySQL. - A migration directory is never deleted: a database that applied it refuses to start without it. ## API stability The public API contract is kept in the repository under `docs/api/`. CI compares every change against the committed baseline, so accidental breaking changes are caught before release. Your frontends can pin the contract by generating a client from it with `hydro sdk generate`. ## Security fixes Fixes land in the latest release and the one before it. Stay within one release of the latest. See [Reporting vulnerabilities](https://hydrogo.dev/docs#security/reporting). > [!TIP] Read `CHANGELOG.md` before every minor upgrade. The Unreleased section shows what is coming. --- # Upgrade steps and rollback *Section: Upgrading* *A safe, repeatable procedure: back up, replace the binary, check, migrate, verify. And how to roll back.* ## Procedure 1. **Back up.** Database and media (see [Backups](https://hydrogo.dev/docs#deployment/backups)). 2. **Replace the binary.** Re-run `install.sh` or the Windows installer, or pull the new image tag. 3. **Preview the change** with `hydro upgrade check`. 4. **Apply migrations** with `hydro migrate`. 5. **Verify** with `hydro doctor`, then start the server. ```bash hydro backup sqlite --destination backups/ --media-dir data/media # 1. back up (or pg_dump / mysqldump) # 2. replace the hydro binary hydro upgrade check --config config.yaml # 3. what will change hydro migrate --config config.yaml # 4. apply migrations hydro doctor --config config.yaml # 5. verify the install hydro serve --config config.yaml ``` Pin a version in automation instead of taking whatever is newest: ```bash HYDRO_VERSION=v0.2.0 sh install.sh # or the matching image tag docker pull ghcr.io/hydrohq/hydrogo:v0.2.0 ``` ## Rolling back `hydro migrate` takes a snapshot of a **SQLite** database before it applies anything. If the upgrade misbehaves, restore it: ```bash hydro rollback --snapshot PATH --confirm --config config.yaml ``` - Reinstall the previous binary version first, then run the rollback. - For PostgreSQL and MySQL, restore the backup you took in step 1; the snapshot mechanism is for SQLite. - Never reverse a migration by deleting tables or editing the migration ledger. > [!WARNING] Rolling back the database discards content written after the snapshot. Put the site in maintenance mode (Site settings, Maintenance) while you upgrade a busy site. ## Zero-downtime notes - Run `hydro migrate` as a separate step before starting new instances, then roll instances one at a time behind your load balancer using `/readyz`. - Keep the previous image tag so you can switch back quickly. - Because HydroGo is stateless apart from the database and media, a blue/green switch is a proxy change. ## Related commands | Command | Purpose | | --- | --- | | `hydro upgrade check` | Show pending migrations and what will change. | | `hydro db diff` | Compare the database with the expected schema. | | `hydro db reindex` | Rebuild database indexes (maintenance). | | `hydro schema diff --baseline A --current B --public-key KEY` | Compare signed release migration ledgers. | | `hydro migration create --id YYYYMMDDHHMM_name --module MODULE --out migrations` | Scaffold a migration (contributors). | --- # Release process *Section: Upgrading* *For maintainers: how a tagged release produces binaries, the Windows installer, checksums and container images.* 1. Move the **Unreleased** notes in `CHANGELOG.md` under a new `## [x.y.z] - date` heading. 2. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`. 3. The release workflow (`.github/workflows/release.yml`) builds binaries for Linux, macOS and Windows (amd64 and arm64), the Windows `Setup.exe`, `SHA256SUMS` and the container image, and creates the GitHub release with that changelog section. 4. Tags containing a hyphen (`v0.2.0-rc.1`) publish as pre-releases and do not move the `latest` image tag. 5. If the public API changed, update `packages/client` and copy it to `examples/sveltekit-starter/src/lib/hydrogo.ts` (a test fails when they differ). Nothing is published to npm. ```bash git tag v0.2.0 git push origin v0.2.0 ``` ## Verifying a download Every release includes `SHA256SUMS`. The install scripts verify it automatically; to check by hand: ```bash sha256sum -c SHA256SUMS --ignore-missing ``` Release manifests can also be verified with the built-in command: ```bash hydro release verify --manifest PATH --public-key PATH [--root PATH] ``` > [!NOTE] Contributors: do not edit a released migration. Add a new one. ================================================================================ # SECTION: REFERENCE ================================================================================ --- # CLI reference *Section: Reference* *Every hydro command, grouped by purpose, with the flags it accepts. Run hydro help for the same list offline.* ```bash hydro help ``` Commands that need a configuration or a database accept the shared **runtime flags** below. Environment variables (see [Environment variables](https://hydrogo.dev/docs#configuration/environment-variables)) fill in anything you do not pass. ## Runtime flags | Flag | Meaning | | --- | --- | | `--config PATH` | Path to `config.yaml`. | | `--capacity-profile compact|standard|distributed` | Capacity profile, see [Modules and profiles](https://hydrogo.dev/docs#configuration/modules-and-profiles). | | `--address HOST:PORT` | Listen address. | | `--database-dialect sqlite|postgres` | Database dialect override (`mysql` is set through config or `HYDRO_DATABASE_DIALECT`). | | `--database-dsn DSN` | Database connection string. | ## Start a project | Command | What it does | | --- | --- | | `hydro new DIR [--preset minimal|blog|corporate|news|docs] [--admin-email EMAIL [--admin-password-stdin]]` | Create a new project folder: config, preset content types, SQLite database and, with `--admin-email`, the first admin. Refuses to overwrite an existing folder. | | `hydro new --list-presets` | List presets. | | `hydro init [same options]` | Scaffold in the current directory. | ## Run and develop | Command | What it does | | --- | --- | | `hydro serve [--cgi | --fastcgi --fastcgi-address 127.0.0.1:9001] [--dev] [runtime flags]` | Start the server (admin, API, workers). `--cgi` and `--fastcgi` are for shared hosting; `--dev` enables development behaviour. | | `hydro run [--address ADDR] [--app-address ADDR] [--database-dsn DSN] [--redis-url URL] [--dependencies auto|none|required] [--poll DUR]` | Development runner that rebuilds and restarts on change. | | `hydro dev [same options]` | Alias for `hydro run`. | | `hydro version [--json]` | Print the version, optionally as JSON. | | `hydro healthcheck` | Probe `/healthz` (override the URL with `HYDRO_HEALTHCHECK_URL`); exit code reflects health. | ## Database | Command | What it does | | --- | --- | | `hydro migrate [runtime flags]` | Apply pending migrations (snapshots SQLite first). | | `hydro rollback [--snapshot PATH] --confirm [runtime flags]` | Restore a SQLite snapshot taken by `migrate`. | | `hydro migration create --id YYYYMMDDHHMM_name --module MODULE --out migrations` | Scaffold a migration directory (contributors). | | `hydro upgrade check [runtime flags]` | Preview what an upgrade will change. | | `hydro schema diff --baseline RELEASE.json --current RELEASE.json --public-key KEY` | Compare signed release migration ledgers. | | `hydro db diff [runtime flags]` | Compare the database with the expected schema. | | `hydro db reindex [runtime flags]` | Rebuild database indexes. | ## Content and users | Command | What it does | | --- | --- | | `hydro content-type new ID [--dir content-types]` | Write a content type skeleton. | | `hydro content-type validate [--dir content-types]` | Validate every content type file. | | `hydro create-admin --email EMAIL --password-stdin [runtime flags]` | Create an admin user; the password is read from stdin. | | `hydro seed --count=20 --locale=tr|en --clean --confirm-demo [--force-prod] [runtime flags]` | Fill a database with demo content. Guarded: refuses production unless forced. | | `hydro clean --demo [--target demo|staging|production] [--force-prod] [runtime flags]` | Remove demo content. | | `hydro sdk generate [--openapi PATH] [--out DIR] [--lang all|typescript|dart]` | Generate a typed client from an OpenAPI document. | ## Operations | Command | What it does | | --- | --- | | `hydro doctor [--json] [--fix] [runtime flags]` | Check the installation; `--fix` applies safe fixes. | | `hydro validate [--json] [runtime flags]` | Validate configuration and database access. | | `hydro ban IP|CIDR [--for 24h] [runtime flags]` | Ban an address or range, optionally for a duration. | | `hydro unban IP|CIDR [runtime flags]` | Lift a ban. | | `hydro backup sqlite|postgres --destination PATH [--media-dir PATH] [runtime flags]` | Create a backup. | | `hydro backup prune --root PATH --keep N --dry-run|--confirm [--pinned NAME,...]` | Delete old backups, keeping the newest N and any pinned names. | | `hydro restore verify --snapshot PATH` | Check a snapshot's integrity. | | `hydro restore unpack --archive FILE.tar.gz --destination NEW_DIRECTORY --public-key HEX [--max-bytes N]` | Unpack and verify a signed archive into a new directory. | | `hydro restore stage --snapshot PATH --destination PATH --sessions-revoked --egress-quarantined --old-writes-blocked` | Stage a restore (guard flags are mandatory). | | `hydro restore postgres --snapshot PATH --destination PATH --sessions-revoked --egress-quarantined --old-writes-blocked [runtime flags]` | Restore a PostgreSQL snapshot. | | `hydro restore activate-media --staging PATH --active PATH --rollback PATH` | Swap restored media into place, keeping the previous directory for rollback. | | `hydro portable export --path PATH [runtime flags]` | Export content as a portable document. | | `hydro portable import --path PATH --dry-run|--confirm [--max-bytes BYTES] [runtime flags]` | Validate or apply a portable document. | | `hydro release verify --manifest PATH --public-key PATH [--root PATH]` | Verify a release manifest. | | `hydro jobs dead-letter list [--limit N] [--json] [runtime flags]` | List failed background jobs. | | `hydro jobs dead-letter requeue --id ID --confirm [runtime flags]` | Retry a failed job. | | `hydro jobs dead-letter purge --older-than 720h --limit N --confirm [runtime flags]` | Delete old failed jobs. | | `hydro tenant validate|list [runtime flags]` | Inspect tenant configuration. | | `hydro tenant provision --tenant-id ID --admin-email EMAIL --admin-password-stdin [runtime flags]` | Create a tenant with its first admin. | | `hydro tenant backfill (--tenant-id ID|--active) [--dry-run|--confirm] [runtime flags]` | Backfill tenant data. | | `hydro scan-links --url https://example.test [--max-pages 500] [--concurrency 4] [--delay 100ms]` | Crawl a site and report broken links. | | `hydro test [--package ./...] [--timeout 5m] [--race] [--count N]` | Run the Go test suite (contributors). | | `hydro events catalog` | List webhook event topics and payload versions. | | `hydro icons --source LOGO --output DIRECTORY` | Generate an icon pack from a logo. | | `hydro help` | Print usage. | ## Common recipes ```bash hydro validate --config config.yaml hydro migrate --config config.yaml && hydro serve --config config.yaml hydro test --package ./modules/news/... --timeout 2m hydro run --address 0.0.0.0:8090 ``` > [!NOTE] Content types need no command to take effect: they are YAML files in `content-types/`. `hydro content-type` only helps you scaffold and validate them. ## Failure remediation | Symptom | Try | | --- | --- | | Config or database errors | `hydro validate --config PATH`, then check dialect, DSN and file permissions. | | Migration errors | Run `hydro migrate` against a disposable database first. Never edit an applied migration; create a new one. | | Test timeout or race failures | Narrow `--package`, raise `--timeout`, reproduce with `go test -run TestName -count=1 ./path`. | | Dev port already in use | Pass another `--address` or `--app-address`, or stop the process that owns the port. | --- # Troubleshooting *Section: Reference* *Symptoms, causes and fixes for the problems people hit most often. Start with the request id.* > [!TIP] Every API error and every audit entry carries a `request_id`. When something fails, copy it: it ties the browser, the proxy and the server log together. ## Command not found `hydro: command not found` right after installing usually means the shell has not picked up PATH yet. Open a **new terminal**. On Windows the installer's "Add hydro to PATH" option must be ticked. Check with `hydro version`. ## Cannot sign in to the admin | Symptom | Cause and fix | | --- | --- | | Login succeeds, then you are signed out immediately | `security.secure_cookies: true` over plain HTTP. Secure cookies are only sent over HTTPS. Use HTTPS, or keep it `false` for local HTTP development. | | Behind a proxy, login loops | The proxy must forward `Host` and `X-Forwarded-Proto: https`, and `http.public_origin` must match the public URL. | | `403` on any POST, PUT or DELETE | Missing CSRF token. Send `X-CSRF-Token` from `GET /csrf-token`, or use a Bearer token or API key instead of the cookie. | | Forgot the password | Another admin can send a reset link from Users and roles. Locked out entirely: `hydro create-admin --email you@example.com --password-stdin` creates a new admin. | | Lost the authenticator device | Use a recovery code from enrolment. Otherwise another admin disables MFA for you. | | `429 too_many_requests` | Login is limited to 5 attempts per minute. Wait for the `Retry-After` period. | ## Start-up errors | Message | Meaning | | --- | --- | | `invalid HYDRO_…: …` | An environment variable has a bad value (for example a boolean). Fix or unset it. | | `automatic TLS cannot run behind a reverse proxy` | `http.auto_tls` and `http.behind_proxy` are mutually exclusive. | | `automatic TLS requires one valid domain` | Set `http.domain` to a single host name without scheme, port or wildcard. | | `preview signing key must contain at least 32 bytes` | Lengthen `security.preview_signing_key`. | | `scheduled cloud backup requires configured S3-compatible storage credentials` | `backup.enabled` needs the `media.s3_*` settings. | | `i18n default locale cannot be empty` | Set `i18n.default_locale` and list it in `i18n.locales`. | | `… already exists; refusing to overwrite` | `hydro new` and `hydro init` never overwrite files. Choose a new folder. | | Address already in use | Another process owns the port. Change `http.address` or stop the other process. | ```bash hydro validate --config config.yaml hydro doctor --config config.yaml ``` ## Database - **database is locked (SQLite).** Only one writer at a time. Make sure no other tool writes to the file, avoid network drives, and move to PostgreSQL or MySQL for concurrent hosting such as CGI. - **Migration fails.** Restore your backup, then run `hydro migrate` against a disposable copy to reproduce. Never edit an applied migration. - **The server refuses to start after deleting a migration folder.** Databases that applied a migration require its directory. Restore it from git. ## Content does not show up | Symptom | Check | | --- | --- | | A new YAML content type is missing in the admin | Run `hydro content-type validate`, then restart `hydro serve`. Unknown YAML keys are rejected. | | `/api/v1/content/` answers 404 | The type needs `public: true`. With `drafts: true` only **published** records are visible. | | Published page is not in the API | Pages and news need the matching `locale` and slug, and must be published (not draft, scheduled for later or expired). | | `400 limit must be between 1 and 100` | Lower `limit`; use `offset` or loop on `metadata.has_more`. | | `400` for `filter[field]` or `search` | The field must be `filterable` (or `searchable`) in the YAML, and each parameter may appear only once. | | `?locale=` answers 400 | The locale is not in `i18n.locales`. | | Translated fields come back in the default language | Localization needs `i18n.enabled` and more than one locale, and the field needs `localized: true`. Missing values fall back per field. | ## Browser and CORS Browsers on another origin can read published content only when the origin is listed in `security.public_api_origins` (no wildcards). Server-side code is unaffected. Admin writes from other origins are not supported; use a token from a server. ## Media - **Uploads rejected.** Free disk space is below `media.min_free_bytes` (default 50 MiB), or your proxy limits the body size (`client_max_body_size` in nginx). - **Images missing after moving servers.** Copy the media directory with the database, or restore both from one backup. - **Hotlinked images blocked on your own site.** Add your frontend host to `media.allowed_referer_hosts`. ## Webhooks - Nothing arrives: check the `topics` list (`hydro events catalog` shows valid names) and that the URL is reachable from the server. - Signature mismatch: sign the **raw** request body, prefixed with the timestamp and a dot, using the same secret. Parse JSON only after verifying. ## Still stuck? Collect the `request_id`, the output of `hydro version`, `hydro doctor --json` (redact secrets) and the relevant log lines, then [open an issue](https://github.com/hydrohq/hydrogo/issues). For security problems follow [Reporting vulnerabilities](https://hydrogo.dev/docs#security/reporting) instead. --- # FAQ *Section: Reference* *Short answers to the questions that come up before and after choosing HydroGo.* ## General ### Does HydroGo render my website? No. It serves JSON and an admin panel. You build the frontend with any technology; see [Frontends](https://hydrogo.dev/docs#frontends/client). ### Do I have to rebuild my site when an editor publishes? No. Publishing is live on the next API request. If you chose a static frontend, a [webhook](https://hydrogo.dev/docs#frontends/caching-and-webhooks) can trigger a rebuild, but that is optional. ### What does it cost? What license is it under? HydroGo is licensed under the PolyForm Shield License 1.0.0. It is free to use, modify and run for your own websites, client applications and e-commerce stores. You may not use it to offer a commercial hosted service or software product that directly competes with HydroGo or Hydro Cloud. ### Which databases are supported? SQLite (default), PostgreSQL and MySQL/MariaDB. Every migration ships for all three. ## Content ### When should I write a content type and when a Go module? Write a YAML content type for structured data: FAQs, team, events, testimonials, products. Write Go only when you need behaviour that is not data, such as special workflows. `modules/news` is the reference for a module with custom behaviour. ### Can I change a content type later? Yes: add fields freely. Renaming or retyping a field is a data migration; add a new field, copy data, then remove the old one. ### Can pages and news be translated? Yes, as separate per-locale documents. Content type fields can be `localized`. See [Content locales](https://hydrogo.dev/docs#multi-language/content-locales). ### Can I use several sites with one installation? There is a `tenancy` configuration for multi-tenant deployments and `hydro tenant` commands. For most projects, one installation per site is simpler and safer. ## Operations ### How do I back up? `hydro backup sqlite|postgres` plus the media directory (or your S3 bucket). See [Backups](https://hydrogo.dev/docs#deployment/backups). ### Can I run more than one instance? Yes. Use PostgreSQL or MySQL, S3 for media and Redis for shared rate limits and caches, and run `hydro migrate` once per release before rolling instances. ### How do I move from SQLite to PostgreSQL? Export with `hydro portable export`, create and migrate the new database, then `hydro portable import --dry-run` followed by `--confirm`. ### Is there a hosted version? HydroGo is software you run yourself on a VPS, container, or shared server. For organizations wanting turnkey managed infrastructure with zero DevOps, Hydro Cloud provides an official multi-tenant cloud overlay. ## Admin ### Which languages does the admin support? English and Turkish today. Each user picks theirs in My Account, and an administrator sets the project default in Site settings. See [Admin interface language](https://hydrogo.dev/docs#multi-language/admin-language). ### Can I theme the admin? The admin follows light, dark or system mode. It is not themeable per project; your public site is entirely yours. --- # Contributing *Section: Reference* *Set up the repository, run the checks, and follow the few architecture rules that keep HydroGo small.* Open an issue for anything bigger than a bug fix, keep changes small and run the checks. By contributing you agree your work is licensed under the repository's PolyForm Shield 1.0.0 license. ## Set up ```bash git clone https://github.com/hydrohq/hydrogo.git cd hydrogo go run ./tools/devserver # rebuilds on save, demo data, Vite HMR for the admin # open http://127.0.0.1:8090 and http://127.0.0.1:8090/admin ``` You need Go 1.27+ and Node 22. Use `-address 0.0.0.0:8090` to open the dev server from a phone on the same network. Optional local PostgreSQL and Redis come from `docker compose -f compose.dev.yaml up -d`. ## Before you push ```bash go run ./tools/check # or: make check go run ./tools/check -short # skip tests npm --prefix admin test # admin unit tests go test ./modules/news/... # one package ``` `check` runs build, vet, gofmt, the repository guards, the Go tests and the admin lint, typecheck, tests and build. If you changed admin UI, also open the screen and look at it at desktop and phone width. ## Architecture rules 1. **Go serves JSON, not markup.** No HTML or CSS in Go strings; the only server-rendered HTML is transactional e-mail. 2. **No customer-specific content in core.** Brand names, menus, dates and demo credentials belong in seed data, config or an example frontend. 3. **Every migration ships all three dialects** and never edits a released migration. Never delete a migration directory. 4. **No SQL dialect or cache details in services.** Keep them in `repository/sqlstore` and `internal/cache`. 5. **An inactive module adds nothing:** no routes, migrations, workers, assets or admin navigation entries. 6. **Keep files small.** Split a file before it passes about 800 lines. 7. **Every API response uses the envelope**, and errors carry the request id. 8. **Every admin endpoint works with all three credentials** through the shared auth middleware. 9. **Zero build on content publish.** Publishing never requires a frontend rebuild, pre-rendering pipeline or redeploy. 10. **Never hide a failing test** by renaming or skipping it. Fix the code, update the test or delete it with an explanation. ## Where changes go | Guard | Rule | | --- | --- | | `tools/codeguard` | No HTML in Go string literals; files stay under 800 lines. Existing violations are frozen in a baseline that may only shrink. | | `tools/migrationguard` | Migration metadata, checksums and dependencies are consistent. | | `tools/moduleguard` | Modules only import what `modules/dependencies.json` declares. | ## Commits and pull requests - Explain the why in the commit message and describe what a user would notice: before, after, then how. - Add a test next to the code you change. - Write a decision record in `docs/decisions/` only when you move a module boundary or change a public contract (routes, API, config, migrations). Keep it under a page. - Do not commit status reports, evidence logs or plans to the repository. ## Decision records | # | Decision | | --- | --- | | 0001 | Refocus HydroGo as a CMS rather than a product factory. | | 0002 | One way to define a content type: a YAML file. | | 0003 | Social sign-in for staff (link existing accounts only) and members (self-service). | | 0005 | Headless core; the admin panel is a Svelte SPA. | | 0006 | Freeze legacy peripherals and the native view pipeline. | | 0007 | Distribute HydroGo as a template CMS: releases, installers, `hydro new`, neutral core. | > [!WARNING] Security issues are not reported in public issues. Follow [Reporting vulnerabilities](https://hydrogo.dev/docs#security/reporting). ## Working on these docs This documentation site is `examples/svelte-starter`. Pages are typed data in `src/lib/docs/*.ts`, one file per section group. Run `npm run dev` in that folder, edit, and the page reloads. ```bash cd examples/svelte-starter npm install npm run dev # http://localhost:3000 npm run check # svelte-check npm run build ``` ================================================================================ # END OF DOCUMENTATION For live updates, visit https://hydrogo.dev or view https://hydrogo.dev/docs