Configuration
All configuration lives in a .env file at the project root.
Environment variables
Section titled “Environment variables”| Variable | Default | Description |
|---|---|---|
JWT_SECRET | required | Min 32-character secret for signing auth tokens. |
CORS_ORIGIN | http://localhost | Comma-separated allow-list of client origins. Use * to allow any (the API is Bearer-token based, no cookies). |
PORT | 80 | Host port for the web interface. |
BACKEND_ORIGIN | backend:3000 | Docker service name:port the frontend proxies /api to — not a host IP. Only change the port, to match a custom backend PORT. |
REGISTRATION | open | Who may create an account: open, first-user, or closed. See below. |
DEMO_MODE | false | Seeds the demo account and sample data. Leave off — its password is public. |
Locking down registration
Section titled “Locking down registration”By default /api/v1/auth/register accepts anyone. That is fine on a LAN and wrong the moment
your instance has a public hostname, which is what the reverse proxy guide gets you.
Set REGISTRATION in your .env and restart:
REGISTRATION=first-userdocker compose up -d # no rebuild — it is read at startup| Value | Behaviour |
|---|---|
open | Anyone can sign up. The default, and what every version before this one did. |
first-user | Open only while no account exists. The first person to register claims the instance; everyone after gets a 403. |
closed | Nobody can register. Where you should end up. |
first-user is a setup mode, not a resting state. Use it for a fresh install — a plain
on/off switch would make you choose between starting open, and racing whatever scanner finds
your hostname, and starting closed, unable to create your own account at all. Then switch to
closed once you have signed up, for the reason below.
A typo is a startup failure, not a fallback: REGISTRATION=frst-user refuses to boot rather than
quietly leaving you wide open. Check docker compose logs backend if the container will not start.
This is enforced by the API, not the interface. The apps also hide the “Create account” link when the server reports registration closed, but that is politeness — the 403 is the lock.
Changing your password
Section titled “Changing your password”Settings → Account → Password, on the web app and in the Android app. It asks for your current password, so a stolen session alone cannot lock you out of your own account.
The web form has its own address, /settings/password, and /.well-known/change-password
redirects to it. That is the W3C Change Password URL,
so Safari’s Keychain, Chrome’s Password Checkup and 1Password can send you straight to the form
instead of dropping you on the front page. Nothing to configure — the bundled nginx serves the
redirect. Behind your own reverse proxy, pass the path through to the frontend like any other
route.
Password length
Section titled “Password length”Between 8 characters and 72 bytes. The minimum is Lyftr’s; the maximum is bcrypt’s, which refuses anything longer rather than silently truncating it.
72 bytes is 72 ordinary characters, but accented letters cost two and most emoji cost four — so a 19-character emoji passphrase is already over. Both apps say so while you type rather than on submit. Worth knowing if your password manager generates long passwords: cap it at 72.
Changing it signs you out everywhere else. The device you changed it on stays signed in. That is deliberate — a password change is what you reach for when you think someone else has a session, so it has to actually end their session. Sign back in on your other devices with the new password.
Other devices stop on their next request to the server: it checks every request against the account’s
current session version, so a device that was signed out gets a 401, fails to refresh, and drops to
the sign-in screen. A device that cannot reach the server at that moment, say on poor wifi, stays on
its old screens until it can. JWT_EXPIRY only sets how often a healthy session re-mints its access
token, not how long a revoked one lingers.
Changing your email
Section titled “Changing your email”Settings → Account → Email, on the web app and in the Android app. Like a password change it
asks for your current password, so a stolen session alone cannot take the address over. The web
form is at /settings/email.
You can change it to any address no other account holds. Letter case does not count as a
difference, so Carter@example.com is taken if carter@example.com is registered, but you can
change the case of your own address. The address is stored exactly as you type it.
Like a password change it signs you out everywhere else. The device you changed it on stays
signed in, and you sign in with the new address from now on. The old one stops working at once.
Lyftr cannot email the old address, so ending the other sessions is how a change made from a session
that was not yours gets contained. Other devices stop on their next request to the server, as with a
password change. Changing only the letter case, say carter@ to Carter@, is the
one exception: sign-in already ignores case, so nothing needs containing and no session ends.
If you forget your password
Section titled “If you forget your password”There is no self-service reset. Nothing here can email you a link, and an instance with no mail server could not send one. Recovery belongs to whoever has a shell on the server — which, since you self-host, is you:
docker compose exec backend ./lyftr-api reset-password you@example.comIt prompts for the new password twice, with no echo, and prints:
Password reset for you@example.com.Every existing session for that account has been signed out.The account and everything in it are untouched — only the password changes.
The password is prompted for rather than passed as a flag, so it never reaches your shell history or the container’s process list.
If you need it unattended, pipe it in. Read it into a variable first rather than typing it into the
command — echo 'your-new-password' | ... puts the password straight into your shell history, which
is the thing the prompt exists to avoid:
read -rs NEWPWdocker compose exec -T backend ./lyftr-api reset-password you@example.com <<< "$NEWPW"unset NEWPWPiped input skips the confirmation prompt — there is nothing to type twice — so whatever arrives on stdin becomes the password, subject only to the same 8-character minimum and 72-byte maximum the apps enforce.
Sessions are signed out because a reset is often prompted by someone else having got in, and a new password is worthless while their existing token still works.
If the instance will not start at all, restoring lyftr.db from a backup predating the password
change also works, at the cost of everything logged since — see Backups.
Deleting an account from the server
Section titled “Deleting an account from the server”An account can be removed from the server’s shell, for the case where nobody can (or should) sign in to it:
docker compose exec backend ./lyftr-api delete-account you@example.comIt prints what will be deleted — workouts, sets, food and weight entries, routines, saved foods — and asks you to type the address again. Anything else aborts with nothing deleted. The address must match letter case exactly; if it does not, the command lists the spellings it found and deletes nothing.
If an upgrade’s boot log says some accounts differ only in letter case, this is how to remove the one you do not want. The case-insensitive index is created on the next start.
If it is the only account, the command warns first: under REGISTRATION=first-user whoever registers
next becomes the owner, and under REGISTRATION=closed nobody can sign in or register. Take a backup
before you run it — see Backups. A device still signed in to the deleted account is signed out on
its next request to the server.
Piping the exact address also works, with -T: echo you@example.com | docker compose exec -T backend ./lyftr-api delete-account you@example.com.
The demo account
Section titled “The demo account”Every version before this one seeded demo@lyftr.local with a password published in this
documentation, on every install. That account is now behind DEMO_MODE, which is off unless
you ask for it.
If you are upgrading, the account you already have is not deleted — deleting it might take real
workouts with it. The backend logs a warning at startup while it exists. To remove it: sign in as
demo@lyftr.local, then Settings → Delete account. If that account has real data in it, change
its password instead — that closes the published-credential hole without losing anything.
Turning registration off does nothing about this: a published password is a working login whatever
REGISTRATION says.
The BACKEND_ORIGIN gotcha
Section titled “The BACKEND_ORIGIN gotcha”BACKEND_ORIGIN is resolved over the internal Docker network, so it must use the backend’s
service name (backend), not your server’s host or LAN IP.
The default compose only exposes the backend on the Docker network — it isn’t published to the
host — so pointing BACKEND_ORIGIN at something like 192.168.1.10:3000 produces:
502 Bad Gatewayconnect() failed (111: Connection refused)If you run the backend on a custom PORT, change only the port (e.g. backend:3008).
Re-syncing exercises
Section titled “Re-syncing exercises”Go to Settings → Exercise Library to see the current exercise count and a seeding progress indicator. Hit Re-sync to pull the latest exercises — it’s a safe upsert, so existing workout data is untouched.