{"id":51230927,"url":"https://github.com/mickamy/adms","last_synced_at":"2026-06-28T16:01:52.662Z","repository":{"id":359336950,"uuid":"1245481003","full_name":"mickamy/adms","owner":"mickamy","description":"PostgREST-style HTTP API for PostgreSQL and MySQL, plus an optional bundled admin UI — all in one binary.","archived":false,"fork":false,"pushed_at":"2026-05-21T12:01:14.000Z","size":49,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-21T19:57:24.912Z","etag":null,"topics":["admin","crud","golang","http-api","mysql","postgresql","postgrest","rest-api","schema"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mickamy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yaml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["mickamy"]}},"created_at":"2026-05-21T09:02:35.000Z","updated_at":"2026-05-21T10:16:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mickamy/adms","commit_stats":null,"previous_names":["mickamy/adms"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/mickamy/adms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mickamy%2Fadms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mickamy%2Fadms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mickamy%2Fadms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mickamy%2Fadms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mickamy","download_url":"https://codeload.github.com/mickamy/adms/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mickamy%2Fadms/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34894560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["admin","crud","golang","http-api","mysql","postgresql","postgrest","rest-api","schema"],"created_at":"2026-06-28T16:01:51.822Z","updated_at":"2026-06-28T16:01:52.655Z","avatar_url":"https://github.com/mickamy.png","language":"Go","funding_links":["https://github.com/sponsors/mickamy"],"categories":[],"sub_categories":[],"readme":"# adms\n\n_Pronounced \"adams\"._\n\nPostgREST-style HTTP API for PostgreSQL and MySQL, plus an optional bundled admin UI — all in one binary.\n\n[![CI](https://github.com/mickamy/adms/actions/workflows/ci.yaml/badge.svg)](https://github.com/mickamy/adms/actions/workflows/ci.yaml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mickamy/adms)](https://goreportcard.com/report/github.com/mickamy/adms)\n[![Codecov](https://codecov.io/gh/mickamy/adms/branch/main/graph/badge.svg)](https://codecov.io/gh/mickamy/adms)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/mickamy?label=sponsor\u0026logo=github)](https://github.com/sponsors/mickamy)\n\n\u003e Status: the read / write API and the bundled admin UI both ship — see the [Roadmap](#roadmap). Remaining items are\n\u003e UI polish (CSV / JSON export, keyboard shortcuts, ER diagram, full a11y pass).\n\n## TL;DR\n\nPoint `adms` at a database and you get two ways in: an HTTP API the frontend can call directly, and an optional\nbrowser-based admin UI hosted from the same binary. No service layer, no codegen, no schema duplicated in two places.\n\n### As an HTTP API\n\n```yaml\n# adms.yaml\ndriver: postgres\ndsn: \"${ADMS_DSN}\"\n```\n\n```sh\nADMS_DSN=\"postgres://postgres@localhost/myapp?sslmode=disable\" adms\n```\n\n```sh\n# list active users, newest first, with their three latest posts embedded\ncurl 'http://localhost:7777/users?status=eq.active\u0026order=created_at.desc\u0026limit=10\u0026select=id,name,posts(id,title)'\n```\n\n```json\n[\n  {\n    \"id\": 42,\n    \"name\": \"alice\",\n    \"posts\": [\n      {\n        \"id\": 1001,\n        \"title\": \"Hello\"\n      },\n      {\n        \"id\": 998,\n        \"title\": \"Notes on B-trees\"\n      }\n    ]\n  }\n]\n```\n\n### As a browser UI\n\n```yaml\n# adms.yaml\ndriver: postgres\ndsn: \"${ADMS_DSN}\"\nui:\n  enabled: true\n```\n\n```sh\nADMS_DSN=\"postgres://postgres@localhost/myapp?sslmode=disable\" adms\n# → open http://localhost:7778/\n```\n\nYou land on a dark-mode admin console with schema-grouped tables in the sidebar, sortable / filterable / pageable row\nviews in the main pane, FK-aware embedded rows, inline editing, typed insert forms, and a built-in schema viewer. No\n`node_modules`, no separate deploy — the UI is embedded in the binary.\n\nThe same idea drives both surfaces: reads, writes, joins, ordering, paging, counting — all defined by your database\nschema.\n\n## Why\n\nTwo friction points in every admin tool you have ever built:\n\n1. **The backend is generic, but you keep writing it.** List endpoints with filters, sorting, paging, related rows,\n   CRUD — the shape is already in the database schema, but every project hand-writes it again.\n2. **The frontend is generic too, eventually.** Once the API exists, the dashboard becomes \"tables with filters and\n   forms\" yet again. Spinning up a React app, picking a component library, and wiring it up is a separate, parallel\n   project.\n\n`adms` collapses both into a single binary. It introspects your database on startup and exposes\na [PostgREST](https://postgrest.org/)-style HTTP API automatically. Set `ui.enabled: true` in the config and the same\nbinary serves a complete admin frontend — no extra deploy, no separate codebase.\n\nThe closest neighbor is PostgREST itself: excellent, but PostgreSQL only and API only. `adms` aims for **PostgreSQL +\nMySQL** and **API + (optional) UI**, with no extra dependencies to install beyond the binary.\n\n## Install\n\nTagged releases are not out yet. Once `v0.1.0` ships:\n\n```sh\n# Homebrew (tap)\nbrew install mickamy/tap/adms\n\n# go install\ngo install github.com/mickamy/adms@latest\n```\n\nWhile unreleased, build from source:\n\n```sh\ngit clone https://github.com/mickamy/adms\ncd adms\nmake build\n./bin/adms --version\n```\n\n## Quickstart\n\n`adms` reads a YAML or TOML config file. With no positional argument it auto-detects `adms.yaml`, `adms.yml`, then\n`adms.toml` in the current directory; otherwise pass the path explicitly (e.g., `adms /etc/adms.yaml`). Strings in the\nconfig are expanded from the environment via `${VAR}` / `$VAR`, so secrets stay out of the file.\n\n### PostgreSQL\n\n```yaml\n# adms.yaml\ndriver: postgres\ndsn: \"${ADMS_DSN}\"\n```\n\n```sh\nADMS_DSN=\"postgres://postgres@localhost:5432/myapp?sslmode=disable\" adms\n```\n\n### MySQL\n\n```yaml\n# adms.yaml\ndriver: mysql\ndsn: \"${ADMS_DSN}\"\n```\n\n```sh\nADMS_DSN=\"user:pass@tcp(localhost:3306)/myapp?parseTime=true\" adms\n```\n\nOn boot, `adms` introspects the target database, builds an in-memory schema model, and starts listening on `:7777`\n(override with `listen:` in the config). Every introspected table becomes a resource at `/\u003ctable_name\u003e`. With\n`ui.enabled: true`, a second listener on `:7778` (override with `ui.listen:`) also serves the bundled admin UI from\nthe same process.\n\n\u003e **Today (Phase 1.5):** `adms` connects, introspects, and prints a per-schema summary, then exits. The HTTP server,\n\u003e health endpoint, and table resources described below land in Phase 2.\n\nVerify it works (Phase 2+):\n\n```sh\ncurl http://localhost:7777/                  # schema dump (JSON)\ncurl http://localhost:7777/healthz           # → \"ok\"\ncurl http://localhost:7777/\u003csome_table\u003e      # first 100 rows as JSON\n```\n\n## The HTTP API\n\n`GET /\u003ctable\u003e` returns rows. Everything else — filtering, projection, ordering, paging, embedding — is driven by URL\nquery parameters. Writes use `POST` / `PATCH` / `DELETE` with JSON bodies. The shape mirrors PostgREST so existing\nclients and mental models transfer.\n\n### Reading data\n\n#### Filters\n\nA filter has the form `?\u003ccolumn\u003e=\u003cop\u003e.\u003cvalue\u003e`. Multiple filters are AND-combined.\n\n| Operator     | Example                | SQL equivalent                                             |\n|--------------|------------------------|------------------------------------------------------------|\n| `eq`         | `status=eq.active`     | `status = 'active'`                                        |\n| `gt` / `gte` | `age=gte.18`           | `age \u003e= 18`                                                |\n| `ilike`      | `name=ilike.AL*`       | `name ILIKE 'AL%'` (MySQL: case-insensitive via `LOWER()`) |\n| `in`         | `id=in.(1,2,3)`        | `id IN (1, 2, 3)`                                          |\n| `is`         | `deleted_at=is.null`   | `deleted_at IS NULL`                                       |\n| `like`       | `name=like.al*`        | `name LIKE 'al%'`                                          |\n| `lt` / `lte` | `score=lt.100`         | `score \u003c 100`                                              |\n| `neq`        | `status=neq.banned`    | `status \u003c\u003e 'banned'`                                       |\n| `not`        | `status=not.eq.banned` | `NOT (status = 'banned')`                                  |\n\nWildcards in `like` / `ilike` use `*` (translated to `%`); `_` remains a single-character wildcard.\n\n```sh\ncurl 'http://localhost:7777/users?status=eq.active\u0026age=gte.18\u0026deleted_at=is.null'\n```\n\n#### Projection (`select`)\n\nBy default, every column is returned. Use `select` to pick columns:\n\n```sh\ncurl 'http://localhost:7777/users?select=id,name,email'\n```\n\nUse `*` to mean \"all columns of this row\":\n\n```sh\ncurl 'http://localhost:7777/users?select=*,created_at'\n```\n\n#### Embedding related rows\n\n`adms` reads foreign keys from the schema and lets you embed related rows by table name in parentheses:\n\n```sh\n# user → posts (one-to-many via posts.user_id → users.id)\ncurl 'http://localhost:7777/users?id=eq.1\u0026select=id,name,posts(id,title,created_at)'\n```\n\n```sh\n# post → author (many-to-one), with an alias\ncurl 'http://localhost:7777/posts?select=*,author:users(id,name)'\n```\n\nEmbeds nest:\n\n```sh\ncurl 'http://localhost:7777/users?select=*,posts(id,title,comments(id,body))'\n```\n\nEmbedded relations resolve to JSON arrays for one-to-many, and JSON objects for many-to-one, derived from the FK\ndirection.\n\n#### Ordering and paging\n\n```sh\ncurl 'http://localhost:7777/users?order=created_at.desc,id.asc\u0026limit=20\u0026offset=40'\n```\n\n`limit` defaults to 100 when omitted and is capped at 1000.\n\n#### Counting rows\n\nTo get a total count alongside the page, send `Prefer: count=exact`:\n\n```sh\ncurl -i -H 'Prefer: count=exact' 'http://localhost:7777/users?limit=20'\n```\n\n```\nHTTP/1.1 200 OK\nContent-Range: 0-19/1342\nContent-Type: application/json\n```\n\n### Writing data\n\nAll write methods accept JSON bodies (`Content-Type: application/json` assumed).\n\n#### Insert\n\n```sh\ncurl -X POST http://localhost:7777/users \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"name\": \"carol\", \"status\": \"active\"}'\n```\n\n```\nHTTP/1.1 201 Created\nLocation: /users?id=eq.42\n```\n\n#### Bulk insert\n\n```sh\ncurl -X POST http://localhost:7777/users \\\n  -H 'Content-Type: application/json' \\\n  -d '[{\"name\": \"dave\"}, {\"name\": \"eve\"}]'\n```\n\n#### Update\n\n`PATCH` requires at least one filter — `adms` rejects an unfiltered `PATCH` with `400 Bad Request` to prevent accidental\ntable-wide updates.\n\n```sh\ncurl -X PATCH 'http://localhost:7777/users?id=eq.1' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"status\": \"inactive\"}'\n```\n\n#### Delete\n\nSame rule as `PATCH`: a filter is mandatory.\n\n```sh\ncurl -X DELETE 'http://localhost:7777/users?id=eq.1'\n```\n\n#### `Prefer` header\n\n| Value                                 | Effect                                      |\n|---------------------------------------|---------------------------------------------|\n| `count=exact`                         | `Content-Range` header with total row count |\n| `return=minimal` (default for writes) | Empty body, `Location` header for inserts   |\n| `return=representation`               | Body contains the affected rows             |\n\n```sh\ncurl -X POST http://localhost:7777/users \\\n  -H 'Content-Type: application/json' \\\n  -H 'Prefer: return=representation' \\\n  -d '{\"name\": \"frank\"}'\n```\n\n```json\n{\n  \"id\": 43,\n  \"name\": \"frank\",\n  \"status\": null,\n  \"created_at\": \"2026-05-21T08:12:00Z\"\n}\n```\n\n### Errors\n\nErrors follow a PostgREST-shaped JSON envelope with adms-specific codes (prefixed `ADMS_`):\n\n```json\n{\n  \"code\": \"ADMS_UNKNOWN_COLUMN\",\n  \"message\": \"column \\\"foo\\\" does not exist in table \\\"users\\\"\",\n  \"details\": null,\n  \"hint\": \"available columns: id, name, status, created_at\"\n}\n```\n\n| HTTP | code                    | When                                   |\n|------|-------------------------|----------------------------------------|\n| 400  | `ADMS_INVALID_FILTER`   | Bad operator or value format           |\n| 400  | `ADMS_UNFILTERED_WRITE` | `PATCH` / `DELETE` without any filter  |\n| 400  | `ADMS_UNKNOWN_COLUMN`   | Column name not in schema              |\n| 403  | `ADMS_READ_ONLY`        | Write attempted while `read_only: true` |\n| 404  | `ADMS_UNKNOWN_TABLE`    | Table name not in (allowed) schema     |\n| 409  | `ADMS_CONFLICT`         | DB-level unique / FK violation         |\n| 422  | `ADMS_INVALID_BODY`     | JSON body fails column-type validation |\n| 500  | `ADMS_INTERNAL`         | Anything unexpected                    |\n\n### Schema endpoint\n\n`GET /` returns the introspected schema as JSON. A frontend (yours or the bundled admin UI) uses this to render forms,\ninfer column types, and discover relations without bundling a schema of its own.\n\n```sh\ncurl http://localhost:7777/\n```\n\n```json\n{\n  \"tables\": [\n    {\n      \"schema\": \"public\",\n      \"name\": \"users\",\n      \"primary_key\": [\n        \"id\"\n      ],\n      \"columns\": [\n        {\n          \"name\": \"id\",\n          \"type\": \"bigint\",\n          \"nullable\": false,\n          \"default\": \"nextval(...)\"\n        },\n        {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"nullable\": false\n        },\n        {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"nullable\": true\n        },\n        {\n          \"name\": \"created_at\",\n          \"type\": \"timestamptz\",\n          \"nullable\": false,\n          \"default\": \"now()\"\n        }\n      ],\n      \"foreign_keys\": [],\n      \"referenced_by\": [\n        {\n          \"table\": \"posts\",\n          \"columns\": [\n            \"user_id\"\n          ],\n          \"references\": [\n            \"id\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n```\n\n## The admin UI\n\nEnabled with `ui.enabled: true` in the config. Off by default, so API-only deployments stay lean. When on, the UI is\nserved on a **separate listener** (`ui.listen`, default `:7778`) by the same process. The API at `:7777` stays\nuntouched, with table names occupying the full URL root. The UI calls the same HTTP API documented above, with CORS\nauto-configured between the two listeners — it is not a parallel implementation, it is the first-class client of it.\n\nThe UI is a single-binary affair: HTML, CSS, and JavaScript are embedded into the `adms` executable via `embed.FS` —\nincluding the tree-shaken minified Tailwind CSS bundle, so deployments in closed networks need no external CDN access.\nNo `node_modules`, no separate frontend deploy. It is rendered server-side with Go's `html/template` and made\ninteractive with vanilla `fetch` against the same HTTP API documented above.\n\n### What you get\n\n- **Sidebar** — schema-grouped table list with incremental search.\n- **Table view** — row list with PostgREST-style filter inputs (kind-aware placeholders, bare values auto-prefixed with\n  the kind-default operator), column-header sort, paging, FK arrows that jump to the referenced row.\n- **Row detail** — type-aware inputs by column kind (`\u003cselect\u003e` for booleans, `type=\"number\"` / `type=\"date\"`,\n  `\u003ctextarea\u003e` for JSON), outgoing FK link that live-updates as you edit, and a \"Referenced by\" section listing\n  incoming relationships as filtered table views.\n- **Edit** — double-click any cell for in-place editing, or open the row in a modal via the row's \"edit\" button. Both\n  paths submit `PATCH /:table?\u003cpk\u003e=eq.\u003cid\u003e` and refresh the visible rows on success.\n- **Insert** — kind-aware form on `/t/{table}/new`. Empty inputs are omitted from the POST so column defaults / NULL\n  apply.\n- **Delete** — confirm dialog → `DELETE /:table?\u003cpk\u003e=eq.\u003cid\u003e`.\n- **Schema viewer** — at `/t/{table}/schema`: columns (name / type / nullable / default / generated-or-identity /\n  comment), primary key, outgoing FKs, incoming FKs (Referenced by), and indexes (name, columns, UNIQUE, method,\n  partial-index predicate). FK / Referenced-by entries link to the other table's schema page.\n\n### Design\n\n- **Dark mode** by default.\n- **Responsive** down to tablet widths (\u003e= 768px).\n- **Read-only gating** — when `read_only: true` the UI hides every write affordance (`+ New`, edit / delete buttons,\n  inline-edit, modal, Save / Delete on row detail) and `/t/{table}/new` returns 404.\n- **Type-aware forms** — boolean / integer / number / date / JSON / text inputs and the JS value parser dispatch on the\n  same Go-side `inputKind` classifier so client and server agree on the column shape.\n\n### Access\n\nThe UI calls the same HTTP API you would. Cross-origin calls between the two listeners are handled automatically —\n`adms` adds the UI's origin to the API's allowed origins, so you do not need to list it in `cors_origins`. When\n`auth_token_env` names a populated env var, the UI carries that token on every request.\nWhen `read_only: true`, the UI hides edit / insert / delete affordances. The UI does not introduce its own login\nflow — keep it behind your network or gateway.\n\n## Security\n\n`adms` is designed to sit behind your authn layer (reverse proxy, API gateway, etc.), but it ships several built-in\nsafety nets so an accidental misconfiguration is not catastrophic.\n\n### Identifier allowlist\n\nTable and column names from query parameters are checked against the introspected schema before they are interpolated\ninto SQL. Unknown identifiers return `400 Bad Request`, never reach the database, and never appear in error messages\nechoed back to the client unsanitized.\n\n### Read-only mode\n\n```yaml\nread_only: true\n```\n\nReturns `403 Forbidden` for `POST`, `PATCH`, and `DELETE`. The admin UI hides write affordances in this mode. Useful for\nstaging dashboards, demos, or anywhere writes must be impossible by construction.\n\n### Schema and table allowlist\n\nRestrict which schemas (or tables) are exposed:\n\n```yaml\nallowed_schemas: [public, reporting]\nallowed_tables: [users, posts, comments]\n```\n\nAnything outside the allowlist is invisible — at `GET /`, at the per-table endpoints, and in the UI sidebar.\n\n### Bearer token\n\n```yaml\nauth_token_env: ADMS_TOKEN\n```\n\nWhen set, `adms` reads the bearer token from the named environment variable and requires every request to include\n`Authorization: Bearer \u003ctoken\u003e`. The admin UI carries the token automatically (the resolved value is exposed via a\nmeta tag that an inline fetch wrapper picks up and attaches to every API-origin request). This is intentionally\nsimple — for OIDC / JWT, terminate auth at your gateway. The token value itself never appears in the config file, so\nit does not leak into version control.\n\n### CORS\n\n```yaml\ncors_origins:\n  - \"https://admin.example.com\"\n  - \"https://staff.example.com\"\n```\n\nDefaults to no CORS headers, so the API is only reachable from same-origin contexts unless you opt in. When\n`ui.enabled: true`, the bundled admin UI's origin is automatically added to the allowed origins — you do not need to\nlist it here.\n\n### Mandatory filters on writes\n\n`PATCH` and `DELETE` without a `where` clause return `400` — there is no \"update every row\" path, in the API or the UI.\n\n## CLI\n\n```\nadms [config-file]\n\nIf the argument is omitted, adms looks for adms.yaml, adms.yml, then adms.toml\nin the current directory. Pass a path to use a specific config file.\n\nFlags:\n  --version, -v   Print version\n  --help, -h      Show help\n```\n\nThe config file is the single source of configuration — there are no per-setting CLI flags or `ADMS_*` reserved\nenvironment variables. Strings in the config file are expanded via `${VAR}` / `$VAR` from the environment so secrets\n(DSN, bearer token, etc.) stay out of source control; the env var names you reference (`ADMS_DSN`, `DATABASE_URL`,\nanything you like) are entirely your choice. Unset variables expand to `\"\"`, and literal `$` cannot be escaped, so\nput values containing `$` in an environment variable.\n\n## Configuration (config file)\n\nA minimal config:\n\n```yaml\ndriver: postgres\ndsn: \"${ADMS_DSN}\"\n```\n\nThe full set of fields, with defaults and meaning:\n\n| Field             | Default          | Description                                                |\n|-------------------|------------------|------------------------------------------------------------|\n| `driver`          | _(required)_     | `postgres` or `mysql`                                      |\n| `dsn`             | _(required)_     | Database connection string (prefer `${VAR}` expansion)     |\n| `listen`          | `:7777`          | API listen address                                         |\n| `read_only`       | `false`          | Reject all write methods with `403`                        |\n| `allowed_schemas` | _(driver default)_ | Schemas to introspect                                    |\n| `allowed_tables`  | _(all)_          | Table allowlist (empty means every introspected table)     |\n| `timeout`         | `30s`            | Startup operation timeout (DSN parsing, introspect, etc.)  |\n| `cors_origins`    | _(none)_         | Allowed origins for CORS                                   |\n| `auth_token_env`  | _(none)_         | Name of the env var holding a bearer token to require      |\n| `log_level`       | `info`           | `debug` / `info` / `warn` / `error`                        |\n| `ui.enabled`      | `false`          | Mount the bundled admin UI                                 |\n| `ui.listen`       | `:7778`          | Listen address for the admin UI                            |\n\nTOML works the same way:\n\n```toml\ndriver = \"postgres\"\ndsn = \"${ADMS_DSN}\"\nlisten = \":7777\"\nread_only = false\nallowed_schemas = [\"public\"]\n\n[ui]\nenabled = false\nlisten = \":7778\"\n```\n\nWorking examples live in [`examples/adms.yaml`](examples/adms.yaml) and [`examples/adms.toml`](examples/adms.toml).\n\n## Roadmap\n\n- [x] Phase 0 — CLI scaffolding, goreleaser metadata\n- [x] Phase 1 — Schema introspection (PostgreSQL + MySQL, including indexes with method and partial predicate)\n- [x] Phase 1.5 — Config-file driven CLI; subcommands and per-setting flags removed\n- [x] Phase 2 — HTTP server, `GET /` schema endpoint, `GET /healthz`, graceful shutdown\n- [x] Phase 3 — Read API: filter (incl. `cs` / `cd` JSON / array containment), projection, ordering, paging\n- [x] Phase 4 — Read API: relation embedding (FK-aware JSON aggregation)\n- [x] Phase 5 — Write API: `POST` / `PATCH` / `DELETE`, `Prefer` header, `Content-Range`\n- [x] Phase 6 — CORS, structured logging, panic recovery, `read_only`, allowlists, bearer token\n- [x] Phase 7 — Bundled admin UI (opt-in via `ui.enabled`): separate listener (`ui.listen`, default `:7778`),\n  HTML/CSS/JS embedded via `embed.FS`, SSR with `html/template` + minified Tailwind, CORS auto-configured, dark mode,\n  type-aware form inputs, inline cell + modal edit, FK navigation, schema viewer, bearer-token forwarding, read-only\n  gating\n- [ ] CSV / JSON export of filtered query results\n- [ ] Keyboard shortcuts (`Cmd/Ctrl+K` table palette, `↑↓` row navigation)\n- [ ] a11y polish (skeleton loaders, explicit empty / error states, light-mode toggle)\n- [ ] Schema viewer ER diagram\n\n## Why not PostgREST?\n\nUse PostgREST if you are PostgreSQL-only and want a battle-tested project with a large community — it is genuinely\nexcellent, and `adms` borrows heavily from its URL conventions.\n\nReach for `adms` when:\n\n- you are on MySQL, or operating a fleet with both PostgreSQL and MySQL, and want one server to manage,\n- you want a UI shipped in the same binary as the API, not a separate frontend project,\n- you prefer a single self-contained Go binary with no Haskell runtime to deploy,\n- you want a tighter scope focused on admin dashboards — opinionated defaults, identifier allowlists, mandatory filters\n  on writes — rather than a general-purpose data API.\n\n## Acknowledgements\n\nThe URL conventions, the embedding syntax, and the `Prefer`-header semantics in this project are taken almost verbatim\nfrom [PostgREST](https://postgrest.org/). The admin UI is styled with [Tailwind CSS](https://tailwindcss.com/),\ngenerated by the standalone tailwindcss CLI and bundled into the binary at build time. Standing on giants'\nshoulders — thank you.\n\n## Sponsor\n\nIf `adms` saves you time, consider supporting ongoing development\nvia [GitHub Sponsors](https://github.com/sponsors/mickamy). Sponsorships pay for the maintenance time that keeps\nPostgres / MySQL parity, security fixes, and roadmap items moving.\n\n## License\n\n[MIT](LICENSE) © 2026 Tetsuro Mikami\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmickamy%2Fadms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmickamy%2Fadms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmickamy%2Fadms/lists"}