2026-07-22 20:38:10 +02:00
2026-07-18 22:58:38 +02:00
2026-07-18 23:22:58 +02:00
2026-01-01 21:07:34 +01:00
2026-01-01 21:07:34 +01:00
2026-05-14 20:51:01 +02:00
2026-05-14 21:31:33 +02:00
2026-07-19 09:58:27 +02:00
2026-01-01 21:48:40 +01:00
2026-01-01 21:07:34 +01:00
2026-07-19 11:29:10 +02:00
2026-07-22 20:38:10 +02:00
2026-07-22 20:38:10 +02:00
2026-07-19 09:58:27 +02:00
2026-05-14 20:51:01 +02:00
2026-07-19 09:58:27 +02:00
2026-05-14 14:13:41 +02:00

L@pp

L@pp is a small, mobile-friendly shared-list application built with Flask. It is designed primarily for grocery lists, but works just as well for chores, packing lists, or any other lightweight checklist.

Users can create and share lists, add items with quantities and categories, and check items off from a phone-friendly interface. The included web app can also be installed as a Progressive Web App (PWA).

Features

  • Multiple lists per user
  • List sharing between registered users
  • Item quantities and units, including input such as Milk 2 cartons
  • Grocery categories with icons
  • Reusable suggestions for previously completed items
  • Bulk item entry
  • Responsive, installable PWA interface
  • User registration with group invitation keys
  • Administrator approval for new accounts
  • Session-based JSON API for companion clients
  • SQLite by default, with other SQLAlchemy database URLs supported

Requirements

  • Python 3.9 or newer
  • pip and Python virtual-environment support

Quick start

Clone the repository and enter its directory:

git clone <repository-url>
cd lapp

Create a virtual environment and install the dependencies:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirement.txt

Initialize the database with an administrator, a regular user, two groups, and some example lists:

python initialize_data.py

The command creates instance/secrets.yaml and instance/app.db. It does not print the generated passwords or registration keys; open instance/secrets.yaml locally to retrieve them. The seeded usernames are admin and ignace.

Start the development server:

python lapp.py

Then open http://127.0.0.1:5001. The built-in server runs in debug mode and is intended for local development only.

Running the application without initialize_data.py creates an empty database schema, but no users or registration groups.

Configuration and secrets

Private configuration lives in instance/secrets.yaml. On first use, missing values are generated automatically and the file is restricted to mode 0600. A generated file has this structure:

runtime:
  secret_key: <Flask session secret>
  application_key: <key used by /api/validate_key>
  database_uri: sqlite:///app.db
initial_data:
  admin_password: <initial admin password>
  admin_group: <initial admin registration key>
  user_password: <initial user password>
  user_group: <initial user registration key>
fernet:
  key: <Fernet key used by test.py>

For SQLite, relative database paths are resolved inside Flask's instance/ directory. To use another database, set runtime.database_uri to a compatible SQLAlchemy URL and install the corresponding database driver.

Keep runtime.secret_key stable. Replacing it invalidates active sessions and the seven-day auto-login cookie. The runtime.application_key is checked by POST /api/validate_key.

The complete instance/ directory is ignored by Git. Back it up securely and never commit or share its contents. In particular, back up both app.db and secrets.yaml together when using the default SQLite setup.

Customizing the initial accounts

To choose credentials instead of using generated values:

  1. Generate the configuration and empty schema without seeding accounts:

    python -c 'from lapp import create_app; create_app()'
    
  2. Edit the values under initial_data in instance/secrets.yaml.

  3. Run python initialize_data.py.

Initialization is deliberately idempotent: if the database already contains a user, no seed data is added. It also refuses to initialize a partially populated database.

How accounts and sharing work

Registration requires a group key. The two keys created during initialization are stored as initial_data.admin_group and initial_data.user_group. A newly registered account remains pending until an administrator approves it at /admin/.

After signing in, a user can create, activate, rename, share, or delete their own lists. A shared list is visible and editable by every selected user. Only the owner can change the list itself or its sharing settings.

Administrators can create additional registration groups from /groups.

Item input

L@pp extracts simple quantities and units from item text. For example:

Input Label Quantity Unit
Apples 4 Apples 4 x
Milk 2 cartons Milk 2 cartons
3 kg potatoes Potatoes 3 kg

Cleaning a list moves checked items into its suggestion history. Those items can then be added again quickly from the add-items screen.

API overview

API requests use form-encoded fields and return JSON. Authentication is stored in the Flask session cookie, so API clients must retain cookies after login.

Method Endpoint Purpose
POST /api/validate_key Validate the configured application key (key)
POST /api/login Sign in with user and password
POST /api/register Register with user and a group key in password
POST /api/load_lists Load lists available to the signed-in user
POST /api/load_items Load active items for listid

Responses use a numeric status field: 1 means success, 0 means failure, and registration uses 2 to indicate that approval is pending.

Example login with a cookie jar:

curl -c cookies.txt \
  -X POST \
  -d 'user=ignace' \
  -d 'password=YOUR_PASSWORD' \
  http://127.0.0.1:5001/api/login

curl -b cookies.txt \
  -X POST \
  http://127.0.0.1:5001/api/load_lists

Production deployment with Nginx and Gunicorn

The following example targets Debian or Ubuntu, installs L@pp in /srv/lapp, and serves it from lists.example.com. Substitute your own installation path and domain where necessary.

Gunicorn loads the existing Flask application factory as lapp:create_app(). The repository's lapp.wsgi file is only needed for a mod_wsgi deployment and is not used here.

1. Install the system packages

sudo apt update
sudo apt install nginx python3 python3-venv

Create a dedicated, unprivileged service account:

sudo useradd --system \
  --home /srv/lapp \
  --shell /usr/sbin/nologin \
  lapp

Place or clone the repository at /srv/lapp, then install the application and Gunicorn:

cd /srv/lapp

sudo python3 -m venv .venv
sudo .venv/bin/pip install --upgrade pip
sudo .venv/bin/pip install -r requirement.txt
sudo .venv/bin/pip install 'gunicorn>=23,<24'

Gunicorn should run as the service account, not as root, and should only be reachable through Nginx.

2. Initialize the application

Create the private instance directory with permissions that allow the service account to maintain the SQLite database:

sudo install -d \
  -o lapp \
  -g www-data \
  -m 0750 \
  /srv/lapp/instance

sudo -u lapp /srv/lapp/.venv/bin/python \
  /srv/lapp/initialize_data.py

View the generated passwords and registration keys locally:

sudo -u lapp sed -n '1,120p' \
  /srv/lapp/instance/secrets.yaml

Back up instance/app.db and instance/secrets.yaml together. Do not expose the instance/ directory through Nginx.

3. Run Gunicorn with systemd

Create /etc/systemd/system/lapp.service:

[Unit]
Description=L@pp service
After=network.target

[Service]
Type=simple
User=lapp
Group=www-data
WorkingDirectory=/srv/lapp

RuntimeDirectory=lapp
RuntimeDirectoryMode=0750
UMask=0007

ExecStart=/srv/lapp/.venv/bin/gunicorn \
    --workers 2 \
    --bind unix:/run/lapp/lapp.sock \
    --access-logfile - \
    --error-logfile - \
    lapp:create_app()

Restart=on-failure
RestartSec=5
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

One or two workers is a sensible starting point for this small SQLite application. Adding many workers can increase SQLite write contention.

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now lapp
sudo systemctl status lapp

Follow its logs or verify that its socket exists:

sudo journalctl -u lapp -f
sudo ls -l /run/lapp/lapp.sock

4. Configure Nginx

Create /etc/nginx/sites-available/lapp:

server {
    listen 80;
    listen [::]:80;

    server_name lapp.suy.nl;

    client_max_body_size 2m;

    location /static/ {
        alias /srv/lapp/static/;
        expires 7d;
        add_header Cache-Control "public";
    }

    location / {
        proxy_pass http://unix:/run/lapp/lapp.sock;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 60s;
    }
}

Enable the site and validate the configuration before reloading Nginx:

sudo ln -s /etc/nginx/sites-available/lapp \
  /etc/nginx/sites-enabled/lapp

sudo nginx -t
sudo systemctl reload nginx

The application should now be available at http://lists.example.com.

5. Enable HTTPS

Once the domain's DNS records point to the server, obtain a TLS certificate with your preferred ACME client. For example, using Certbot:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d lists.example.com

Do not expose L@pp publicly over plain HTTP: it handles passwords and persistent login cookies.

Updating a deployment

Back up the database and secrets first. Then update the source and dependencies and restart Gunicorn:

cd /srv/lapp
sudo .venv/bin/pip install -r requirement.txt
sudo systemctl restart lapp
sudo systemctl status lapp

See the official Flask Gunicorn deployment guide, Gunicorn documentation, and Nginx proxy module documentation for additional configuration options.

Utility scripts

  • initialize_data.py safely seeds a new database.
  • show_db.py prints all database records for local diagnostics. Its output includes password hashes and group secrets, so treat it as sensitive.
  • update_db.py contains historical, manual migration snippets. Review and adapt it before running it against any database.
  • test.py is a Fernet encryption example, not an automated test suite.

Project structure

lapp.py                 Application factory and blueprint registration
auth.py                 Login, registration, logout, and password changes
admin.py                Account approval
groups.py               Registration-group management
lists.py                List creation, ownership, and sharing
items.py                Item entry, categories, quantities, and completion
api.py                  Session-based JSON API
models.py               SQLAlchemy models
templates/              Jinja templates
static/                 CSS, JavaScript, icons, and PWA manifest
instance/               Local secrets and database (generated, ignored by Git)

Development notes

There is currently no automated test suite or migration framework. Before upgrading an existing installation, back up the database and secrets, then test the change on a copy of the data.

S
Description
list app
Readme
931 KiB
Languages
Python 66.8%
HTML 28.5%
JavaScript 4.7%