{"id":50684071,"url":"https://github.com/ronpinkas/csat","last_synced_at":"2026-06-08T21:01:57.431Z","repository":{"id":362378258,"uuid":"1258541538","full_name":"ronpinkas/csat","owner":"ronpinkas","description":"Self-contained CSAT survey + analytics in a single Go binary — encrypted SMS survey links, admin dashboard, i18n (en/es), per-deployment branding","archived":false,"fork":false,"pushed_at":"2026-06-03T23:28:17.000Z","size":1268,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T00:17:53.505Z","etag":null,"topics":["csat","customer-satisfaction","go","self-hosted","single-binary","sqlite","survey"],"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/ronpinkas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"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":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T17:18:00.000Z","updated_at":"2026-06-03T23:28:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ronpinkas/csat","commit_stats":null,"previous_names":["ronpinkas/csat"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ronpinkas/csat","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ronpinkas%2Fcsat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ronpinkas%2Fcsat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ronpinkas%2Fcsat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ronpinkas%2Fcsat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ronpinkas","download_url":"https://codeload.github.com/ronpinkas/csat/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ronpinkas%2Fcsat/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34080026,"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-08T02:00:07.615Z","response_time":111,"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":["csat","customer-satisfaction","go","self-hosted","single-binary","sqlite","survey"],"created_at":"2026-06-08T21:01:52.507Z","updated_at":"2026-06-08T21:01:57.421Z","avatar_url":"https://github.com/ronpinkas.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CSAT\n\n[![CI](https://github.com/ronpinkas/csat/actions/workflows/ci.yml/badge.svg)](https://github.com/ronpinkas/csat/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nA self-contained, **configurable** survey + analytics app in a single Go binary. Ships a CSAT\ninstrument by default; define your own questions in a `survey.json`.\n\n- **Public survey** reached via a tokenized link (e.g. an SMS after a support call, an email\n  after an order). The questions are defined in `survey.json` — **stars, scales, NPS,\n  single/multi-choice, free text** — each with per-language labels.\n- **Admin dashboard** (authenticated) that **auto-adapts to your questions**: per-question KPIs,\n  distributions, NPS, breakdowns, daily trend, recent comments, and CSV export — all over a\n  selectable date range.\n- **One binary.** SQLite (pure-Go), all HTML/CSS/JS + Chart.js embedded. No runtime, no\n  `node_modules`. Cross-compiles to a static Linux binary from any OS.\n\n## Screenshots\n\nAdmin dashboard — **auto-adapts to your questions**: per-question KPIs, NPS, distributions,\nchoice/multichoice breakdowns, daily trend, and recent comments, over a selectable date range:\n\n![Admin dashboard](docs/screenshots/dashboard.png)\n\nThe customer survey (here the rich `survey.example.json` — stars, NPS, choice, multichoice,\nscale, text), localized by the link's language token, with a per-deployment logo + theme:\n\n| English | Español |\n|:---:|:---:|\n| ![English survey](docs/screenshots/survey-en.png) | ![Spanish survey](docs/screenshots/survey-es.png) |\n\n## Install (one line)\n\nOn a Linux server:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/ronpinkas/csat/main/install.sh | sudo sh\n```\n\nIt detects your OS/arch, downloads the matching release, **verifies its SHA-256**, and installs the\nbinary + a `systemd` service. It also installs an updater (`csat-update`) you can run anytime; the\nnightly **auto-update timer is off by default** — opt in with `CSAT_AUTOUPDATE=1`. Pin a version\nwith `CSAT_VERSION=v1.0.0`. macOS installs just the binary (no service); Windows users download the\nzip from [Releases](https://github.com/ronpinkas/csat/releases).\n\n\u003e Prefer to read first? `curl -fsSL .../install.sh -o install.sh`, inspect it, then `sudo sh install.sh`.\n\nAfter install: edit `/etc/csat/config.toml`, drop a `logo.*` into `/etc/csat/`, set the admin\npassword in `/etc/csat/csat.env`, `sudo systemctl enable --now csat`, then front it with TLS (see\n`deploy/`). The token secret to share with your link-builder is shown on the admin **Settings** page.\n\n### Updating\n\nUpdate on demand with `sudo csat-update` — it fetches the latest release within the current major\nline, backs up the database, verifies the checksum, swaps the binary, and restarts. Config, secret,\nlogo, and data are untouched. To automate it nightly (opt-in), enable the timer:\n`sudo systemctl enable --now csat-update.timer`. A new major (e.g. `v2`) is always left for you to\napply deliberately.\n\n## Build\n\n```sh\nmake build          # local binary -\u003e dist/csat\nmake build-linux    # static linux/amd64 -\u003e dist/csat-linux-amd64 (CGO disabled)\nmake package        # release tarball -\u003e dist/csat-\u003cversion\u003e-linux-amd64.tar.gz\nmake test           # run the test suite\n```\n\nThe Linux binary is a single statically-linked file (everything embedded, no runtime). `make\npackage` bundles it with config templates, the systemd unit, and an installer — see\n[`INSTALL.md`](INSTALL.md).\n\n**Prebuilt downloads:** each tagged release attaches archives for **Linux, macOS, and Windows\n(amd64 + arm64)** — see the [Releases](https://github.com/ronpinkas/csat/releases) page. Tag a\nversion (`git tag v1.0.0 \u0026\u0026 git push --tags`) and the release workflow builds and publishes them.\n\n## Run (local)\n\n```sh\ncp config.example.toml config.toml      # edit site name, db path, etc.\ncp .env.example .env                     # leave CSAT_CRYPTO_SECRET unset to auto-generate\ndist/csat -config config.toml\n```\n\nOn first boot it creates the SQLite DB, runs migrations, seeds the `admin` user (password from\n`CSAT_ADMIN_INITIAL_PW`, force-changed on first login), and — if no `crypto_secret` is set —\ngenerates a unique token secret and prints it (also visible at `/settings`).\n\nMint a test link without the call platform:\n\n```sh\ndist/csat -config config.toml -mint -subject \"+15551234567\" -ts 1717286400 -base \"http://localhost:8080\"\n```\n\n## The token contract (for the call platform's link-builder)\n\nThe SMS link is `https://\u003chost\u003e/s?t=\u003ctoken\u003e`. The platform builds `\u003ctoken\u003e`; this app only\nvalidates it. The token **encrypts** the caller id and call time (nothing sensitive appears in\nthe URL) and is self-authenticating.\n\n```\nkey   = SHA-256(crypto_secret)                            // 32 bytes\npt    = subject + \"|\" + subjectTimeUnixSeconds + \"|\" + lang  // none of the fields may contain \"|\"\nnonce = 12 random bytes\nct    = AES-256-GCM_Seal(key, nonce, pt)                  // no associated data\ntoken = base64url_nounpad( nonce || ct )                  // ct includes the 16-byte tag\n```\n\n`subject` is whatever the survey is about — a phone number, order id, ticket id, … `lang` is the\nsurvey language: `en` or `es` (anything else falls back to `en`). The `crypto_secret` is the\nper-deployment value shown on the admin **Settings** page; use the same value on both sides.\nThere is **no expiry**; each link is **single-use** (a second submit for the same subject+time is\nrejected). Ready-made link builders are in [`integrations/`](integrations/) (Python + Node).\n\nGenerate a test link:\n\n```sh\ndist/csat -config config.toml -mint -subject \"+15551234567\" -ts 1717286400 -lang es -base \"http://localhost:8080\"\n```\n\n## Survey definition (`survey.json`)\n\nThe questions are data, not code. Point `[survey] definition` at a `survey.json` (or use the\nbuilt-in default). Each question has a `key`, a `type`, per-language `label`s, and `required`:\n\n| type | renders as | stored | dashboard |\n|---|---|---|---|\n| `stars` | 1–`max` stars | number | average + top-box % + distribution |\n| `scale` | `min`–`max` buttons (+ end labels) | number | average + distribution |\n| `nps` | 0–10 buttons | number | **NPS score** + distribution |\n| `choice` | radios | value | breakdown donut |\n| `multichoice` | checkboxes | values | breakdown donut |\n| `text` | textarea | text | recent comments |\n\n```json\n{\n  \"version\": 1,\n  \"intro\":  { \"en\": \"Thanks for your call. How did we do?\", \"es\": \"Gracias por su llamada. ¿Cómo lo hicimos?\" },\n  \"thanks\": { \"en\": \"Thank you!\", \"es\": \"¡Gracias!\" },\n  \"questions\": [\n    { \"key\": \"recommend\", \"type\": \"nps\", \"required\": true,\n      \"label\": { \"en\": \"How likely are you to recommend us?\" },\n      \"ends\":  { \"low\": { \"en\": \"Not likely\" }, \"high\": { \"en\": \"Very likely\" } } },\n    { \"key\": \"topics\", \"type\": \"multichoice\", \"label\": { \"en\": \"What did we discuss?\" },\n      \"options\": [ {\"value\":\"billing\",\"label\":{\"en\":\"Billing\"}}, {\"value\":\"tech\",\"label\":{\"en\":\"Technical\"}} ] }\n  ]\n}\n```\n\nSee [`survey.example.json`](survey.example.json) for the full default. System strings (buttons,\nerrors, \"thank you\") come from the built-in `en`/`es` catalog; question wording lives in the\nsurvey.json. Add a language by adding its key to each label map.\n\n## Branding\n\nPer-deployment branding lives in `[branding]` (see `config.example.toml`):\n\n- **Logo** — just drop a file named `logo.svg` / `logo.png` / `logo.webp` / `logo.jpg`\n  (also `.jpeg/.gif/.bmp`) next to `config.toml`. It's auto-detected and served at\n  `/branding/logo`, resolved per request — adding, replacing, or removing it takes effect with\n  no restart. (`logo_dir` changes where to look; `logo_path` sets an explicit override.)\n- **Theme color** — `theme_color` (hex) drives buttons and selected states, served via\n  `/branding/theme.css` so the strict no-inline-styles CSP still holds.\n\nThese show on the survey, done/error, and login pages.\n\n## Deploy\n\nSee [`deploy/README.md`](deploy/README.md) for the systemd + reverse-proxy setup. In short:\ndrop the binary at `/usr/local/bin/csat`, config at `/etc/csat/config.toml`, secrets at\n`/etc/csat/csat.env` (both `chmod 600`), enable the `csat.service` unit, and point your reverse\nproxy at the listen address (or set `server.tls.mode = \"autocert\"` to terminate TLS in-process).\n\n## Layout\n\n```\ncmd/csat            entrypoint (config, wiring, graceful shutdown, -mint helper)\ninternal/config     TOML + .env loader, env: indirection, secret resolution\ninternal/db         SQLite open (WAL) + embedded migrations\ninternal/token      AES-256-GCM survey-link tokens\ninternal/surveydef  survey.json schema (types, i18n) + embedded default\ninternal/survey     dynamic form rendering + one-time submission\ninternal/admin      auth, sessions, invites, analytics, settings, CSV export\ninternal/httpx      server, security headers/CSP, rate limiting, logging\ninternal/web        embedded templates + static assets (incl. vendored Chart.js)\n```\n\n## Security model\n\nThere is nothing secret in this repository. Each deployment's safety rests on its own\n`crypto_secret` (auto-generated per deployment, never committed) — the link tokens are AES-256-GCM\nsealed with `SHA-256(crypto_secret)`. Publishing the source does not weaken any deployment; rotate\nthe secret if it is ever exposed. Report security issues privately to the maintainer rather than via\npublic issues.\n\n## License\n\nMIT — see [LICENSE](LICENSE). Bundles Chart.js (MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fronpinkas%2Fcsat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fronpinkas%2Fcsat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fronpinkas%2Fcsat/lists"}