{"id":16380007,"url":"https://github.com/0ffz/clicky","last_synced_at":"2026-05-15T01:05:58.674Z","repository":{"id":41909863,"uuid":"204598135","full_name":"0ffz/Clicky","owner":"0ffz","description":"A site for easily creatable voting rooms with 2-10 options","archived":false,"fork":false,"pushed_at":"2022-12-28T04:38:13.000Z","size":136,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-01T20:22:53.530Z","etag":null,"topics":["django","python","vote-application"],"latest_commit_sha":null,"homepage":"https://clicky.offz.me","language":"Python","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/0ffz.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}},"created_at":"2019-08-27T01:59:14.000Z","updated_at":"2022-12-28T04:37:29.000Z","dependencies_parsed_at":"2023-01-31T06:01:31.380Z","dependency_job_id":null,"html_url":"https://github.com/0ffz/Clicky","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ffz%2FClicky","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ffz%2FClicky/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ffz%2FClicky/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ffz%2FClicky/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0ffz","download_url":"https://codeload.github.com/0ffz/Clicky/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240035445,"owners_count":19737601,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["django","python","vote-application"],"created_at":"2024-10-11T03:50:09.677Z","updated_at":"2026-05-15T01:05:58.666Z","avatar_url":"https://github.com/0ffz.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n\u003cimg alt=\"Icon\" width=\"500\" src=\"assets/banner.webp\"/\u003e\n\u003c/p\u003e\n\u003ch3 align=\"center\"\u003eSimple live polls for large audiences\u003c/h3\u003e\n\nClicky is a simple tool to poll large groups of people and display results live. A presenter can create a room, define a list of options, and have an audience join via a QR code on screen.\n\n_This applicatcion is built with Ktor and HTMX, with updates sent live using Server Sent Events. See the [Development](#development) section for more information._\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"600\" src=\"assets/application.png\"\u003e\n\u003c/p\u003e\n\n## Self-hosting\n\nClicky runs as a single JVM process, with rooms stored in memory. Restarting the app will kick users back to the homepage, but the upside is no database or filesystem setup is needed at all! We provide a Docker image for easy setup, a full list of environment variables with descriptions can be found in [application.yaml](https://github.com/0ffz/Clicky/blob/master/server/src/main/resources/application.yaml)\n\n```yaml\nservices:\n  clicky:\n    image: ghcr.io/0ffz/clicky:latest\n    ports:\n      - \"8080:8080\"\n    environment:\n      # Root URL of the site, used for QR codes\n      SITE_URL: \"https://example.com\"\n      # SITE_CHART_UPDATE_INTERVAL: 0.2\n      # SITE_RATE_LIMIT: 100\n      # SITE_INACTIVE_ROOM_TIMEOUT: 24\n```\n\nTo get HTTPS encryption on your website, use a reverse proxy such as traefik, caddy, or nginx.\n\n# Development\n\nBuild commands:\n\n- Run local server: `./gradlew run`\n- Build Docker image: `./gradlew publishImageToLocalRegistry`\n\n## Architecture\n_This app was originally an experiment rewriting an old [Django](https://github.com/0ffz/Clicky-django) application I made using HTMX, I will try to link to a blog post here once finished._ Below is a quick overview of the app architecture:\n\n- Ktor serves pages using the Kotlin HTML DSL. They include HTMX as a small JavaScript dependency and style components using Tailwind classes (with the stylesheet generated by a small Gradle plugin.)\n- We add HTMX tags to set up a Server Sent Event (SSE) connection. When state changes, the server renders partials using the HTML DSL which contain `id` tags that tell HTMX which components to swap.\n- State for each room is stored in a ViewModel-like class, votes are cast by updating `MutableStateFlow` objects. This class also exposes flows for the rendered components using Molecule to listen to state changes using Compose.\n  - NOTE: This logic doesn't really belong in a ViewModel, but we want to avoid re-rending the same components for each client, so we instead keep one flow and collect it in our `Ktor` route. It would be interesting to expand on this concept by providing an API to merge our Molecule composables with initial page generation so we could almost get the same experience as a normal compose app :thinking:.\n\n## Architecture references\n\n- [Ktor SSE docs](https://ktor.io/docs/server-server-sent-events.html) for using SSE on the backend.\n- [HTMX SSE docs](https://htmx.org/extensions/sse/) for establishing a connection on the client.\n- [Molecule](https://github.com/cashapp/molecule) aids in collecting state flows on the backend and rendering component updates on state changes.\n- [Kotlinx.collections.immutable](https://github.com/Kotlin/kotlinx.collections.immutable/) lets us update large maps and lists efficiently, while keeping the simple `.update` syntax of MutableStateFlows.\n- [TailwindCSS](https://tailwindcss.com/) is used via its CLI + a gradle task _(nag Offz about publishing this plugin if you're interested in using it)_.\n- [Lucide icons](https://lucide.dev/icons/), which are inserted as SVGs.\n- [HTMX + SSE post by anderssv](https://blog.f12.no/wp/2024/11/11/htmx-sse-easy-updates-of-html-state-with-no-javascript) for inspiring the partial function used and showing off a similar architecture.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0ffz%2Fclicky","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0ffz%2Fclicky","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0ffz%2Fclicky/lists"}