Member, meeting, and show management for LUGs and Fan CoLab groups
Run your LEGO® User Group in one place: members, meetings, shows, check-ins, dues, volunteers, build challenges, inventory and money, with Discord (optional) and Google Calendar built in. Self-hosted, open source, and light enough for a small home server.
Who it's for:
- LEGO User Groups (LUGs), adult fan (AFOL) clubs, and other LEGO fan communities, big or small.
- Groups recognized in LEGO Fan CoLab (formerly the LEGO Ambassador Network) as a Recognized LEGO Fan Community. You can record your Community Ambassador and their history, keep a yearly Fan CoLab to-do list, and print or download the year's activity summary. Your reports show your recognition.
Try the demo → It's a fictional "Brickton LUG". Switch between a member, a moderator and an admin from the yellow bar. Nothing you do there is saved.
Notice: This project was built entirely using AI (Claude Code by Anthropic). All code, documentation, templates, and configuration were generated through AI-assisted development.
Members
- Member records with age range (KFOL/TFOL/AFOL), contact details and per-field privacy. Each member decides who sees their email, phone, address, birthday and Discord name.
- Sign in with an email and password, with Discord, or with an emailed link. Two-factor codes from an authenticator app (with recovery codes), and an option to require them for organisers or everyone. Forgot-password emails and admin-made set-password links. Members who sign in with Discord get their verified email filled in, emails members enter themselves are confirmed by a link, and people stay signed in while they keep using the site (30 days after their last visit).
- With Discord: anyone in your server can sign in, and Discord roles can map to admin. Without Discord: add members with their email address and send them a set-password link.
- Dues: a payment history, automatic expiry, an "expiring soon" list, renewal reminders and bulk updates.
- Perk levels: yearly attendance tiers that hand out Discord roles automatically.
- Young-member consent: guardian contact, signed consent and photo release, flagged at check-in.
- My Account: notification choices, signed-in devices, download my data, delete my account.
- Merge duplicate members, for example when someone added by hand later signs in with Discord.
Meetings and shows
- Meetings (in person or Discord voice) and multi-day events, with recurring schedules ("2nd Tuesday at 7 PM").
- RSVPs with capacity and a waitlist that moves people up automatically.
- Volunteer shifts, display (MOC) table requests, and a visitor tap counter for public shows.
- QR self check-in, a venue kiosk screen, and attendance by the people running the event.
- Event photos, Markdown notes, and reports posted to a Discord forum.
- A printable per-event report, suited to LEGO Fan CoLab reporting (formerly the LEGO Ambassador Network).
- An annual report covering growth, retention, venues and a five-year trend.
- A LEGO Fan CoLab page: Community Ambassador history, a yearly to-do list with a dashboard reminder, and the year's activity summary as a printable page or CSV.
Discord, calendars and the public
- Discord (optional): announcements, forum threads, scheduled events, reminders, role sync and DMs, all kept in step as meetings and events change. Choose what's posted (per area, and per meeting or event), a quiet mode, an activity log of everything sent with retry for failures, and nickname options. Switch Discord off entirely and messages go by email. Times in Discord messages are Discord timestamps, so each member sees them in their own time zone.
- Your own wording for every message: Discord posts, messages to members and sign-in emails, with placeholders and a live preview (Settings > Message wording).
- A Schedule page with meetings and events together, as a list or a month calendar, filtered by type, dates, scope (chapter, Group-wide, External), status, "Mine" and search.
- Google Calendar sync, a public iCal feed, per-chapter feeds and a private personal feed, with one-click subscribe buttons (Apple/iPhone, Google, Outlook.com, Microsoft 365, QR code). Each meeting and event has "Add to calendar", and reminder emails carry an .ics attachment.
- A public "upcoming shows" page you can embed on your website, with an "I plan to come" button.
- A public About page about your group, written in the app with a what-you-see-is-what-you-get editor (headings, lists, links, photos), with the past year's highlights.
- Email (optional SMTP) for members who aren't on Discord, with a no-login unsubscribe link in every message.
- An optional weekly digest: one Monday message per member covering their week.
Running the LUG
- Chapters for groups with sub-groups. Turn them off if you don't have any.
- Build challenges with entries and voting, then a winner announcement on Discord.
- Inventory: tables, baseplates and display cases.
- Storage locations (the storage unit, the show trailer, someone's garage), each with a person who looks after it. Items can be split across several locations.
- Check-outs come from a location and are returned to it, with due-date reminders.
- A photo and a condition (good / worn / needs repair / broken) for each item.
- A pack list on each event, with what to bring, where it's kept, and a printable checklist to tick off.
- Treasury: income, expenses, receipts and a yearly balance, with a treasurer role that isn't full admin.
- Feature toggles: switch off anything your LUG doesn't use. Its data is kept.
- First-run setup, an audit log of every change, CSV exports, daily backups (photos included) and a JSON API.
- Live pages: when someone changes something, everyone else's open pages update by themselves. If you're in the middle of editing, an "Updated - refresh" button waits for you instead. (Behind a reverse proxy, allow websockets.)
- Dark mode, colour themes (Classic, plus Creators, Communities and Events in the colours of fancolab.lego.com; each member picks theirs, admins set the default), phone-friendly pages, and installable as an app.
LUG Manager is a single C++ server with SQLite, shipped as a Docker image (ghcr.io/arklug/lug-manager). The one-line version, for any machine with Docker:
mkdir lug-manager && cd lug-manager
curl -O https://raw.githubusercontent.com/ArkLUG/lug-manager/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/ArkLUG/lug-manager/main/.env.example
# edit .env: Discord app/bot credentials and LUG_PUBLIC_URL (see Configuration)
docker compose up -d
docker compose logs lug-manager | grep setupThen:
- Create the first admin. On a fresh install the log prints a one-time link,
/setup?token=…. Open it and enter your name and Discord user ID (or an email address).- Alternative: set
BOOTSTRAP_ADMIN_DISCORD_ID, and that Discord account becomes admin on its first sign-in.
- Alternative: set
- Sign in and follow the setup checklist. A dashboard banner takes you to Settings > Setup, which covers:
- your LUG's name and time zone
- which features you use
- your Discord server and announcements channel
- which Discord roles are admins
- optionally, your LEGO Fan CoLab recognition and Community Ambassador
- Put it behind HTTPS with a reverse proxy, and set the public address (Settings > Email & address, or
LUG_PUBLIC_URL). Cookies are marked secure when it'shttps://.
Data lives in the /app/data volume: the database, uploads and backups.
Other ways to host it: see HOSTING.md for:
- DigitalOcean, AWS, Google Cloud, Azure, Linode or Hetzner, with a one-paste setup file that includes automatic HTTPS
- Unraid, with a template included
- Fly.io (
fly.toml) - Render (one-click blueprint)
- Railway
DOCKER.md covers compose, nginx, Kubernetes and backups.
Everything that isn't a secret or needed before the server starts is set in the app under Settings. The public address, the email server and the Discord interactions key can also come from environment variables: then the environment wins, and Settings shows that field as set on the server. Environment variables are read from the environment or from .env; real environment variables always take precedence over .env.
| Variable | Needed | What it's for |
|---|---|---|
DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET |
With Discord | Discord sign-in (OAuth2 app) |
DISCORD_REDIRECT_URI |
Optional | Defaults to the public address + /auth/callback |
DISCORD_BOT_TOKEN |
With Discord | Bot for announcements, roles, DMs and member sync |
LUG_PUBLIC_URL |
Optional | The public address, e.g. https://lug.example.org; or set it in Settings > Email & address. Emailed links (sign-in, password reset, email confirmation) are only sent once there is one. |
BOOTSTRAP_ADMIN_DISCORD_ID |
Optional | Makes this Discord account admin on first sign-in. The /setup link works too. |
DISCORD_PUBLIC_KEY |
Optional | Lets duplicate-member matches be resolved from Discord buttons; or set it in Settings > Discord matches |
LUG_SMTP_URL, LUG_SMTP_USER, LUG_SMTP_PASSWORD, LUG_SMTP_FROM |
Optional | The email server. Usually set in Settings > Email & address instead; a variable that is set wins there |
LUG_PORT, LUG_DB_PATH, LUG_TEMPLATES_DIR |
Optional | Defaults 8080, ./lug.db, ./src/templates. The Docker image sets these. |
ICAL_TIMEZONE, ICAL_CALENDAR_NAME, DISCORD_GUILD_ID |
Optional | Starting values; change them later in Settings |
LUG_OFFLINE |
Never in production | 1 blocks every outbound request (Discord, Google, email). Use it for testing and copies of real data. |
LUG_DOTENV |
Optional | 0 skips reading .env |
Switch Discord off under Settings > Features (or untick it in the setup checklist). Then:
- Create the first admin from the
/setuplink with an email address and a password. - Add members with their email addresses, and send each one a set-password link from their member page (Settings > Sign-in has the options, including required two-factor).
- Notifications go by email once an email server is set up (Settings > Email & address).
- Create an application at discord.com/developers/applications.
- OAuth2:
- Add the redirect URI
https://your-host/auth/callback. - Copy the Client ID and Client Secret.
- Add the redirect URI
- Bot:
- Create the bot and copy its token.
- Turn on the Server Members Intent, which member sync needs.
- Invite the bot:
- Scopes:
bot,identify,guilds. - Permissions: Manage Events, Create Public Threads, Send Messages, Manage Roles, Manage Channels.
- Scopes:
- In your server's role list, drag the bot's role above every role it should assign.
- Optional: to resolve duplicate-member matches from Discord buttons, set
DISCORD_PUBLIC_KEYandDISCORD_APPLICATION_ID. Then registerhttps://your-host/discord/interactionsas the Interactions Endpoint URL.
Members are synced from Discord every 6 hours. A Discord member who may already have a record (for example, a kid added by a parent) is held for review rather than duplicated.
What gets posted is set under Settings > Discord: for each new meeting (announcement, Discord event) and event (thread, announcement, chapter copy, Discord event, update notes), pings, DMs and challenge winners. Each meeting and event can leave parts out for itself. Quiet mode posts nothing new (it only edits and removes), for imports or maintenance. Every post, edit, deletion and failure is in Activity log, where failed posts can be retried. Repair times fixes Discord copies of times sent by older versions without notifying anyone.
- Create a Google Cloud service account and download its JSON key.
- Enable the Calendar API.
- Share your calendar with the service account's email address as an editor.
- Enter the key path and the Calendar ID under Settings > Google Calendar.
Private meetings and events still appear on the calendar, but only as "Private LUG Event", with no title, place or description.
Set the email server and the public address under Settings > Email & address: pick your provider (Gmail, Microsoft 365, Outlook.com, iCloud, Fastmail, Zoho, SendGrid, Mailgun, Amazon SES or other) to fill in the server, then the user name, password and the address to send as. The password is stored encrypted with a key in data/secret.key and never shown again. "Send me a test email" sends one straight away and shows the mail server's error if it fails. The LUG_SMTP_* and LUG_PUBLIC_URL variables still work and win when set. Then:
- Email me a link and Forgot it? work on the sign-in page (single use, short-lived, rate limited);
- members who can't get a Discord DM (no Discord, DMs off, or Discord switched off) get reminders, waitlist notices, digests and dues reminders by email.
Every email has a no-login unsubscribe link and one-click List-Unsubscribe headers.
| Role | Can |
|---|---|
| Member | See meetings, events and members (contact details follow each member's privacy choices); RSVP, volunteer, enter challenges, add photos, manage their own account. |
| Moderator (and Chapter Lead) | Everything a member can, plus: add and edit members, manage dues, see all contact details, run meetings and events, review Discord matches. |
| Admin | Everything, including settings, roles, features, reports, deleting members and merging duplicates. |
| Treasurer (flag, any role) | The Treasury page and recording dues there. |
How roles are assigned
- Admin and Member can come from Discord role mappings. Moderator and Chapter Lead are set by hand.
- A role that came from Discord follows Discord: losing the Discord role removes it. A role set by hand is never lowered by sync.
Chapter roles (only when Chapters is on)
- A chapter's leads manage its members and event managers.
- Event managers run that chapter's meetings and events.
- Event leads manage one specific event.
- With chapters off, "Chapter Lead" is no longer offered. Moderator gives the same permissions.
Privacy: each member chooses, field by field, between Don't share, Verified members (members who have attended in person or paid dues) and All members. Moderators and admins always see everything.
Settings > Features (admin). A switched-off feature disappears from the sidebar, pages and forms, its pages return 404, and its reminders stop. Its data is kept, and the JSON API isn't affected.
Features you can switch off:
- Discord (sign-in, sync, posts and DMs; messages then go by email)
- Chapters
- Membership dues
- Perk levels
- Event RSVPs
- Display requests
- Volunteer shifts
- Event photos
- Build challenges
- Inventory
- Treasury
- Recurring meetings
- Reports
- LEGO Fan CoLab
- Kiosk and visitor counter
- QR self check-in
- Calendar feeds
- Discord reports
- Young-member consent
These start off and are opt-in:
- Weekly digest
- Public shows page
- Public About page
- Daily database snapshots go to
data/backups; the newest 14 are kept by default. Settings > Backups has "Back up now" and download links. - Uploads: photos and receipts are copied to
data/backups/uploadswith each backup. You can also download them all as one.zip. - Members' own data: every member can download it as JSON, or delete their account, from My Account.
- Secret key:
data/secret.keyencrypts the email password stored in the database. It's not in the database backups, so copy it along when moving to a new server (otherwise just re-enter the email password). - Audit log: records who did what, when, and from which IP address.
Full create/read/update/delete under /api/v1/*:
- Covers: members, events, meetings, chapters, attendance, perk levels, role mappings, settings, the audit log and Discord match review.
- Keys: admins create API keys under Settings > API Keys, each with a
read,writeoradminscope. Send the key asX-API-Key: <key>orAuthorization: Bearer <key>. Only a hash of each key is stored. - Docs: interactive docs at
/static/api-docs.html, with the spec at/static/openapi.yaml.
curl -H "X-API-Key: $LUG_API_KEY" https://lug.example.org/api/v1/eventsStack:
- Server: C++20 and CrowCPP, with SQLite in WAL mode and 64 automatic migrations.
- Pages: server-rendered Mustache templates with htmx and Tailwind CSS.
- JavaScript libraries: DataTables, Tom Select, EasyMDE, driver.js and QRCode.js, all bundled in the repo with no CDN.
- Security: a strict nonce-based Content-Security-Policy.
- Other libraries: libcurl, OpenSSL, zlib, nlohmann/json and md4c.
# Ubuntu/Debian: sudo apt-get install cmake g++ pkg-config libcurl4-openssl-dev libsqlite3-dev libssl-dev zlib1g-dev
# Arch: sudo pacman -S cmake gcc pkgconf curl sqlite openssl zlib
# macOS: brew install cmake curl sqlite openssl pkg-config
cmake -B build -S . -DBUILD_TESTS=ON
cmake --build build -j$(nproc)
bash scripts/build_css.sh # Tailwind -> src/static/tailwind.min.css
LUG_OFFLINE=1 ./build/lug_manager # http://localhost:8080ctest --test-dir build --output-on-failure -j$(nproc)There are 58 test suites, unit and integration. Tests never contact real services:
- The test fixture sets
LUG_OFFLINE=1. - Discord and Google Calendar run against local fakes that record every request. These are
tests/support/fake_discord.hppandtests/support/fake_google.hpp, built ontests/support/fake_server.hpp. - Email uses a capturing mailer.
Browser checks: these need Firefox and Selenium. Point them at a running server with LUG_BASE.
tests/e2e/smoke.py <session-token> <screenshot-dir>covers every page and pop-up, and fails on any CSP violation or JavaScript error.tests/e2e/mobile.pyloads the main pages at phone size and fails if any page scrolls sideways.
Safety rules for local runs:
- Use
LUG_OFFLINE=1for development and for any copy of real data. - Don't start test servers from a folder whose
.envholds real credentials. Real environment variables take precedence over.env, andLUG_DOTENV=0skips it. LUG_DISCORD_BASEandLUG_GOOGLE_BASEpoint the clients at a fake server, and are accepted only for loopback addresses.
The workflow .github/workflows/demo.yml builds the demo and publishes it to GitHub Pages:
- When: on every push to
main, every Monday so the dates stay current, and on demand. - How:
scripts/demo/seed.pycreates the fictional LUG.scripts/demo/export.pycrawls a throwaway offline server once per role and saves its pages and panels as static files.scripts/demo/demo.jsmaps the app's requests onto those files and blocks anything that would save.
- Checking it:
tests/e2e/demo_check.pyclicks through the published site.
To build the demo locally:
bash scripts/demo/build.sh build/lug_manager /tmp/demo-site /lug-manager
mkdir -p /tmp/site && ln -s /tmp/demo-site /tmp/site/lug-manager
python3 -m http.server 8000 --directory /tmp/site # http://localhost:8000/lug-manager/Discord is one chat::Provider (src/chat/Provider.hpp, implemented in src/integrations/discord/DiscordProvider.hpp). chat::ChatHub decides what to post and keeps it in step; a provider only knows how to talk to its service. To add one:
- Implement
chat::Provider: post/edit/delete messages, threads (or reportcaps().threads = false), scheduled events (orfalse), direct messages, where things go (place(), chapter channels), and mention syntax.inert()must make member-supplied text unable to ping anyone. - Register it next to Discord in
register_all_routes(svc.chat->add(...)), and add aFeaturesentry and a settings page for its channels. - Its posts are recorded in
chat_posts, its switches arechat.<id>.<name>settings, and the message templates are shared. Test it liketests/unit/test_chat_hub.cppdoes with a recording provider.
Code is grouped by area, and every code folder has a CMakeLists.txt that lists its files (add new files there). Everything except main.cpp builds into the lug_core library, which the server and the tests link.
src/
main.cpp start-up, background jobs (sync, reminders, digest, backups)
routes/ HTTP handlers by area: accounts/ members/ events/ meetings/
attendance/ community/ settings/ pages/, and api/
(/api/v1: members/ events/ admin/)
services/ business logic: events/ members/ notifications/, plus
features, audit, backups, photos, Fan CoLab
repositories/ SQLite access: members/ events/ admin/
integrations/ one folder per outside service:
discord/ REST client, OAuth, chat provider; sync/ (members,
roles), matches/ (match queue), time_repair/
google_calendar/ email/ ical/
chat/ chat-service layer: Provider interface, ChatHub, templates
auth/ sessions, passwords, two-factor
middleware/ auth, CSP and security headers, feature gating
db/ models/ config/ async/ utils/ (text/, web/)
templates/ Mustache pages and partials
static/ CSS/JS (vendored), app.js behaviours, OpenAPI docs
sql/migrations/ applied automatically at start-up
tests/
unit/ no server
integration/<area>/ the whole app on a local port, against fake services
support/ test fixture and the fake Discord/Google servers
e2e/ browser checks (Python + Selenium)
scripts/ Tailwind build, theme CSS generator, demo site
cmake/ build helpers (Crow template patch)
unraid/ Unraid Community Applications template
deploy/ one-paste cloud-init setup for VPS providers
fly.toml, render.yaml cloud hosting configs (see HOSTING.md)
The LUG Manager logo:
- Logo Concept by Brigs
- Final Design by Alexandria B (ROCLUG)
- "Missing Permissions" (50013) from Discord: move the bot's role above the roles it manages.
- Sign-in fails: check that
DISCORD_REDIRECT_URImatches the redirect in the Developer Portal exactly, and that the person is in your Discord server. - Nothing shows on Google Calendar: check that the key file path is readable, that the service account has editor access, and that the Calendar ID looks like
…@group.calendar.google.com. - Templates not found: run from the project folder, or set
LUG_TEMPLATES_DIR. - API returns 401 or 403: the key may be revoked, or it may lack the right scope. Role mappings and settings need
adminscope, even to read.
GNU Affero General Public License v3.0. You may fork, modify and share LUG Manager, but changes you run as a network service must be shared under the same license.
Pull requests are welcome. Please make sure ctest passes, the code builds without warnings, and new features come with tests.
LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project. LUG Manager is an independent, fan-made tool and is not part of LEGO Fan CoLab.