Parcella is an open-source web application for managing allotment garden associations AKA ("Kleingartenverein" / "Schrebergarten" associations): members, parcels, lease administration, mandatory work hours and much more.
Started as a vibe-coding project for we became sick of these ill-architectured proprietary software - hopefully generic enough for any allotment garden association, in any country. If you miss anything, feel free to drop a line at https://forum.parcella.org - or just fix it yourself here on Github.
- ✅ a responsive design so you can use it on your smart phone during your garden inspections, water meter reading etc...
- ✅ strict separation of association and lease
- ✅ REST API with JWT authentication and Swagger documentation
- ✅ Database migrations via Alembic
- ✅ a certain testing suite
- ✅ cloud storage of your choice for file dropping: Nextcloud, WebDAV or S3-compatible
- ✅ CMS integrations for your beloved website
- ✅ bullet proof runs against shannon and strix (for now)
- ✅ switch off and sort modules like "electricity" as you like it
📖 Detailed documentation, which later might become an HTTP online help.
This project is licensed under the GNU Affero General Public License v3.0 (see LICENSE). In particular, this means: anyone who runs a modified version of this software as a network service (e.g. SaaS for other associations) must make the source code of that modified version publicly available. Details and contribution guidelines in CONTRIBUTING.md.
- ✅ Session-based login (cookie-based)
- ✅ Invitation system (no public sign-up)
- ✅ Advanced rights and roles via a group system: Admin, board and other users enjoy fine-grained access to modules.
- ✅ Member management (core data, multiple phone numbers, multiple email addresses, IBAN, soft delete for Admin/Board)
- ✅ Parcel management (status: active/terminated/deleted, area, termination)
- ✅ Many-to-many member ↔ parcel assignment, multiple parcels per member; every resident of a parcel is held jointly responsible, with no hierarchy between them
- ✅ CSV export and import (members, parcels) with duplicate detection
- ✅ Dashboard with live statistics (members, parcels, areas, plus open purchase requests and open helpdesk conversations when those modules are enabled)
- ✅ Public signup API: lets an external CMS (WordPress, TYPO3, Contao, or anything else) submit work-session signups without a Parcella login, identifying only by parcel number (never a member name - the public site must not expose who lives where). Parcella matches the optional submitted name against the parcel's current residents and registers just that member if it's unambiguous, or every current resident as a precaution if it isn't, creating real participants the board can review and correct like any other signup. Off by default (it opens a public write endpoint) and protected by a regenerable shared token; a reference WordPress connector plugin (
parcella-connector) is included underintegrations/wordpress/, consolidating every WordPress <-> Parcella integration (currently signup; more planned) behind one shared settings screen
- ✅ Club settings (A/B/C area sizes, lang settings, SMTP configuration and much more)
- ✅ Custom club branding: upload your own logo and set your club's display name from Admin -> Settings, replacing the default tree icon and "Parcella" placeholder everywhere in the sidebar
- ✅ i18n: 7 languages (German, English, Polish, Czech, Slovak, French, Dutch), one language per installation, switchable in admin settings, every module and the navigation fully translated. JSON translation catalogs, English as the base/authoring language and the runtime fallback for any missing key.
- ✅ l10n: region and currency are independent settings from language (e.g. an English-language UI can still show German number formatting and EUR). Number and money formatting (correct decimal/thousands separators and currency symbol position per region); address display order adapts per region (continental postcode-before-city vs. UK-style postcode-last).
- ✅ Work-hours system (year-based configuration, configurable per parcel or per member)
- ✅ Work sessions (standard and special), participant management with hours tracking
- ✅ Task backlog for work sessions: create tasks, optionally schedule to a session, optionally assign to a specific signed-up participant (workload label only - no member ability/health data stored, task matching stays a manual coordinator decision)
- ✅ Sponsorships (flat-rate hour credit for area coordinators)
- ✅ Club roles / extended board with work-hours exemption
- ✅ Annual work-hours report with CSV export
- ✅ Water and electricity metering (metering points, meters, readings, consumption reports)
- ✅ Property and accident insurance tracking per parcel, with annual report
- ✅ Calendar module: community calendar (member meetings, parcel inspections, work sessions - all in one simple upcoming-items list, no full calendar-grid UI), member birthdays (with a dashboard "this week" widget highlighting round-number birthdays), council on-site presence scheduling, and self-service council absence logging (anyone with a login can enter their own). Each with its own ICS export - the community calendar's feed is public (embeddable on your public website), the other three require a private access token since they contain more sensitive information
- ✅ No built-in ticket system -- the club's helpdesk is the canonical system for support conversations, chosen from a dropdown on Admin -> Integrations, one at a time: FreeScout, Zammad, Znuny / OTRS, osTicket, Zendesk or Freshdesk. Each one gets exactly the features its API supports (osTicket, for example, can only take new requests from the website contact form). Every logged-in member gets a "Support" nav link straight to it; staff additionally get a Parcella-side conversation list with automatic member/parcel matching (persistent identity mapping, never guessed when ambiguous), a live-fetched full message transcript, reply-to-customer, and one-click promotion of a conversation onto the task board. Off by default. See
docs/module-support.md.
- ✅ Purchase requests with a two-person approval principle (two distinct board members must approve before a purchase is made)
- ✅ Author a piece of club news once (Markdown body, image, optional print override) and deliver it to all three channels - a paced email send to current members with notifications enabled (plus a one-off test send), a draft on the club's website, and a one-page branded PDF that auto-shortens and adds a QR code to the published website post if the full text doesn't fit. The website is chosen from a dropdown on Admin -> Integrations, one at a time: WordPress (its own REST API), a Markdown folder on cloud storage for static sites (Hugo, Jekyll, Eleventy, Astro, ...), or any CMS with a Parcella plugin following the open website plugin protocol. Ready-made, GPL-licensed plugins for Drupal, Joomla, TYPO3 and Contao are in
integrations/. Parcella only ever creates drafts; publishing stays a human decision. Off by default, admin/board only. Seedocs/module-announcements.md.
- ✅ An asset register for everything the club owns (and personal items members store on club property, tracked with the same financial fields for insurance/liability purposes), grouped into freely-configurable categories, with a quantity-aware lending system for borrowable items (checkout/return, a suggested per-loan fee, a board-wide "who has what out right now" view). Full REST API alongside the web UI. See
docs/module-inventory.md.
- ✅ Lets board/admin browse, upload to, and download from a per-parcel document folder in the club's own cloud storage (lease agreements, membership paperwork); the same storage holds incoming invoices, contact documents, backups and the Markdown website. Choose it from a dropdown on Admin -> Integrations, one at a time: Nextcloud, any WebDAV server (ownCloud, Seafile, HiDrive, Hetzner Storage Box, ...) or S3-compatible storage (AWS, Hetzner Object Storage, MinIO, Wasabi, ...). Off by default, admin/board only; who can see a folder's contents is managed directly in the storage, not by Parcella. See
docs/module-cloud-storage.md.
- ✅ A general kanban board (To Do / In Progress / Done) for club business that isn't tied to a work session, with drag-and-drop reordering. Admin/board only, separate from the work-hours module's session-scoped task backlog. Full REST API alongside the web UI. See
docs/module-tasks.md.
| Component | Technology |
|---|---|
| Backend | Python 3.12 + FastAPI |
| Templates | Jinja2 (server-side rendering) |
| CSS | Bootstrap 5 |
| Database | PostgreSQL 16 |
| Migrations | Alembic |
| i18n/l10n | JSON translation catalogs + Babel (number/currency formatting) |
| Calendar/ICS | icalendar (RFC 5545 feed generation) |
| Announcements | markdown (authoring), bleach (sanitizing), WeasyPrint (PDF), qrcode (print QR codes) |
| Container | Docker + docker compose |
git clone https://github.com/parcella-garden/parcella.git
cd parcella
cp .env.example .env
# adjust .env as needed (passwords, SMTP, etc.)# only if you work on Linux. Please do not do this on Mac or Windows
echo "UID=$(id -u)" >> .env
echo "GID=$(id -g)" >> .envdocker compose -f docker-compose.dev.yml build web
docker compose -f docker-compose.dev.yml run --rm --entrypoint alembic web upgrade head
docker compose -f docker-compose.dev.yml up -dThe application is now available at http://localhost:8000; documentation: https://github.com/parcella-garden/parcella/tree/main/docs
An admin account is created automatically on first startup:
- Email:
admin@parcella.local - Password:
admin1234
Alongside the web UI, there is a full REST API under /api/v1/.
Interactive documentation:
- Swagger UI: http://localhost:8000/api/docs
- ReDoc: http://localhost:8000/api/redoc
- OpenAPI schema (JSON): http://localhost:8000/api/openapi.json
# Request a token
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@parcella.local", "password": "admin1234"}'
# Response: {"access_token": "...", "token_type": "bearer", "expires_in_minutes": 1440}
# Use the token
curl http://localhost:8000/api/v1/members \
-H "Authorization: Bearer <access_token>"Tokens are valid for 24 hours. The Swagger UI has an "Authorize" button for convenient testing.
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/auth/login |
Request token (JSON) |
| GET | /api/v1/auth/me |
Retrieve own profile |
| GET | /api/v1/stats |
Dashboard statistics |
| GET | /api/v1/members |
List members (search, pagination) |
| GET | /api/v1/members/{id} |
Retrieve member incl. parcels |
| POST | /api/v1/members |
Create member |
| PUT | /api/v1/members/{id} |
Update member (partial update) |
| DELETE | /api/v1/members/{id} |
Delete member (soft delete) |
| POST | /api/v1/members/{id}/phones |
Add phone number |
| POST | /api/v1/members/{id}/emails |
Add email address |
| GET | /api/v1/parcels |
List parcels (status filter) |
| GET | /api/v1/parcels/{id} |
Retrieve parcel incl. members |
| POST | /api/v1/parcels |
Create parcel |
| PUT | /api/v1/parcels/{id} |
Update parcel (also status/termination) |
| POST | /api/v1/parcels/{id}/assignments |
Assign a member |
| DELETE | /api/v1/parcels/{id}/assignments/{aid} |
Remove assignment |
| GET | /api/v1/club-settings |
Retrieve club settings |
| PUT | /api/v1/club-settings/{key} |
Set a setting (admin/board only) |
| GET/PUT | /api/v1/work-hours/configuration/{year} |
Work-hours configuration |
| GET/POST/PUT/DELETE | /api/v1/work-hours/club-roles |
Club roles + assignments |
| GET/POST/PUT/DELETE | /api/v1/work-hours/sessions |
Work sessions + participations |
| GET/POST/PUT/DELETE | /api/v1/work-hours/tasks |
Task backlog, scheduling, assignment |
| GET/POST/PUT/DELETE | /api/v1/work-hours/sponsorships |
Sponsorships |
| GET | /api/v1/work-hours/evaluation/{year} |
Annual report |
| GET/POST/PUT/DELETE | /api/v1/water/metering-points |
Water metering points + meters |
| POST | /api/v1/water/metering-points/{id}/meter/exchange |
Exchange water meter |
| GET/POST/DELETE | /api/v1/water/metering-points/{id}/readings |
Water readings |
| GET | /api/v1/water/evaluation/{year} |
Water consumption report |
| GET/POST/PUT/DELETE | /api/v1/electricity/metering-points |
Electricity metering points + meters (same shape as water) |
| GET/POST/PUT/DELETE | /api/v1/insurance/packages |
Property insurance packages |
| GET/PUT | /api/v1/insurance/configuration/{year} |
Accident insurance amounts |
| GET/PUT | /api/v1/insurance/parcels/{id}/{year} |
Insurance status of a parcel |
| GET | /api/v1/insurance/evaluation/{year} |
Annual report |
| GET | /api/v1/helpdesk/conversations |
List the active helpdesk's open conversation links |
| GET | /api/v1/helpdesk/conversations/{id} |
Conversation link detail |
| PUT | /api/v1/helpdesk/conversations/{id}/member |
Set/clear the matched member |
| PUT | /api/v1/helpdesk/conversations/{id}/parcel |
Set/clear the matched parcel |
| POST | /api/v1/helpdesk/conversations/{id}/task |
Create/link a task on the task board |
| POST | /api/v1/helpdesk/conversations/{id}/reply |
Reply to the customer through the helpdesk |
| GET/PUT | /api/v1/helpdesk, /api/v1/website, /api/v1/cloud-storage |
Active helpdesk / website / cloud storage and their settings (admin; secrets never returned) |
/api/v1/freescout/conversations... |
Deprecated alias of the helpdesk endpoints with the old field names | |
| GET/POST | /api/v1/purchase-requests |
List/create purchase requests |
| POST | /api/v1/purchase-requests/{id}/approve |
Approve (two distinct approvals needed) |
| POST | /api/v1/purchase-requests/{id}/reject |
Reject (single rejection is enough) |
| GET | /api/v1/public/work-sessions/upcoming |
Public, unauthenticated: upcoming sessions for external CMS forms |
| GET | /api/v1/public/parcels |
Public, unauthenticated: parcel list for external CMS forms |
| POST | /api/v1/public/work-sessions/signup |
Public signup, requires X-Parcella-API-Token header |
Write access (POST/PUT/DELETE) requires the role admin, board, or
treasurer. Read access is available to all authenticated users
(including readonly).
Schema changes go through Alembic rather than automatic create_all().
# Apply migrations (also runs automatically on container startup)
docker compose -f docker-compose.dev.yml run --rm --entrypoint alembic web upgrade head
# Generate a new migration after a model change
docker compose -f docker-compose.dev.yml run --rm web alembic revision --autogenerate -m "Short description"For an existing installation predating Alembic: see MIGRATION-NOTE.md.
Running a club's own instance doesn't require cloning the whole repo or building the image yourself. Versioned images are published to GHCR on every release. Grab just the two files you need:
mkdir parcella && cd parcella
curl -O https://raw.githubusercontent.com/parcella-garden/parcella/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/parcella-garden/parcella/main/.env.example
# edit .env: passwords, SECRET_KEY, ENVIRONMENT=production, SMTP, etc.
# Postgres self-heals ownership on its data dir, but the app image runs as
# a fixed non-root user (uid/gid 1000) and does not -- pre-create and own
# this one so the app can write to it on first start:
mkdir -p data/postgres data/uploads
sudo chown -R 1000:1000 data/uploads
docker compose run --rm --entrypoint alembic web upgrade head
docker compose up -dEverything the running instance writes to disk -- Postgres data, uploaded
logos/avatars/announcement images -- lives under this one data/ folder
next to the compose file. No named Docker volumes, no -f flags:
docker-compose.yml is Compose's own default-discovered filename, so
every command in this guide, and /admin/system's "update available"
notice, works with bare docker compose pull / up -d. Pin
PARCELLA_VERSION in .env to a specific release for a reproducible
deploy instead of always tracking latest.
Upgrading from an older install (named postgres_data volume, or no
data/uploads mount yet)? See
MIGRATION-NOTE-DATA-LAYOUT.md
for the one-time manual migration -- back up first.
If your setup needs anything docker-compose.yml doesn't already cover
-- most commonly running behind a reverse proxy (add --proxy-headers --forwarded-allow-ips=* to uvicorn's command so it trusts
X-Forwarded-* and reports the right client IP/scheme) or a different
port -- put that in a
docker-compose.override.yml next to docker-compose.yml, not by
editing docker-compose.yml itself.
Because the base file is named docker-compose.yml, Compose auto-merges
a docker-compose.override.yml sitting next to it automatically -- no
-f flag needed for either file, on any command:
docker compose pull
docker compose up -dList-valued settings (e.g. ports) merge by combining, not
replacing -- if you just restate ports: in the override, you get
both the base file's port mapping and yours, not a swap. Use the
!override tag to fully replace one instead:
# docker-compose.override.yml
services:
web:
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips=*
ports: !override
- "127.0.0.1:8082:8000"--proxy-headers alone is not enough: uvicorn then only trusts
forwarded headers from 127.0.0.1, but a host nginx reaches the container
through the Docker network gateway (e.g. 172.19.0.1). Without
--forwarded-allow-ips, every visitor looks like that one address, so
the login throttle and the public form rate limits collapse into a
single bucket -- a handful of failed logins from anywhere locks everyone
out. * is safe here only because the port is bound to 127.0.0.1,
so nothing but the local proxy can reach the app.
(A custom Postgres data path is no longer a common reason to need an
override -- the base file already bind-mounts it under ./data/postgres
by default. Only add a db: block here if you need a different host
path than that.)
Why this matters for updates: docker compose pull only refreshes
the image web: points at -- it does nothing for the compose file's
own content, since that isn't something Docker pulls at all. If you
hand-edit docker-compose.yml directly (e.g. change image: to
build:, or just add flags inline) instead of using an override file,
that edit is permanent: nothing in the documented update flow, or in
/admin/system's notice, will ever touch it again. Worse, pull/up
will keep running without any error either way, so a stale or
locally-diverged docker-compose.yml fails silently -- it looks
like the update succeeded while nothing actually changed. Keeping
customizations in a separate override file means docker-compose.yml
itself never needs local changes, and a future curl -O re-fetch of it
(if you ever need one) is always safe.
For production, set ENVIRONMENT=production:
SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(48))')
ENVIRONMENT=production
POSTGRES_PASSWORD=<secure-password>Both are enforced, not just recommended: with ENVIRONMENT set to anything other than development, the app refuses to start while SECRET_KEY is still the built-in default (which is published in this repository and signs every session cookie and API token). Setting ENVIRONMENT=production is also what makes the session and CSRF cookies HTTPS-only and enables HSTS. Keep SECRET_KEY with your backups - changing it later logs everyone out and makes stored SMTP/Nextcloud credentials unreadable.
Recommended: put Nginx in front as a reverse proxy with Let's Encrypt (Certbot).
server {
listen 443 ssl;
server_name verwaltung.myassociation.example;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Needed for login throttling and the signup API's rate limit to
# see the real client address; run uvicorn with --proxy-headers
# to trust it (only ever behind a proxy you control).
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}users – application users (not club members)
invitations – email invitation tokens
members – club members
member_phones – n phone numbers per member
member_emails – n email addresses per member
parcels – garden parcels
member_parcels – m:n member <-> parcel assignment (with metadata)
club_settings – key-value store for club master data
work_hours_configuration – year-based hours/rate configuration
club_roles – club offices (board, extended board, etc.)
member_club_roles – member -> club role assignment (year-based)
work_sessions – standard and special work sessions
session_participations – who attended which session (with hours)
sponsorships – area responsibilities (flat-rate hour credit)
change_history – generic audit log for field changes
metering_points, meters,
meter_readings – water/electricity metering
property_insurance_packages,
insurance_configuration,
parcel_insurance – insurance tracking per parcel/year
helpdesk_conversation_links – helpdesk conversation cross-reference (see docs/module-support.md)
helpdesk_customer_ids – member <-> customer id, per helpdesk
purchase_requests,
purchase_request_approvals – purchase requests with two-person approval
