Answer "yes" to the reverse proxy question. Caddy then stays off, and the app listens on 127.0.0.1:3000.
Send every path of your domain to that address, for example with Caddy:
crm.example.com {
reverse_proxy 127.0.0.1:3000
}When your proxy runs in its own container, 127.0.0.1 is not the host. Set APP_BIND=0.0.0.0 in deploy/.env, block port 3000 in your firewall, and point the proxy at the host address. Then run docker compose up -d in the deploy folder.
Move to another domain
install.sh writes the domain you gave it. Nothing changes it later, so a domain you add in front of an existing install does not reach the app. The sign in page still works, but every address the app prints for a provider, an SSO callback or an OAuth redirect names the old host.
Three values carry the domain and they must change together. Edit deploy/.env:
RELOOP_DOMAIN=crm.example.com
APP_URL=https://crm.example.com
API_URL=https://crm.example.comRELOOP_DOMAIN is the name Caddy asks a certificate for. Change only API_URL and Caddy keeps serving the old certificate.
APP_URL takes a comma separated list. Keep the old host on it while you move, so a bookmark and an open session still work:
APP_URL=https://crm.example.com,https://old-host.example.comThen restart:
cd deploy && docker compose up -dBefore you restart, add the new redirect URI at every provider you connected. Google, Microsoft and Slack each reject a callback they do not know, so sign in through that provider breaks the moment the app starts naming the new host. Keep the old URI as well until the move is done. The addresses are printed in Settings, Connections.
Add a colleague
The owner and every admin add people in the app. Open Settings, Members and choose Add person. Enter the address, the name and the role. The CRM creates the account and shows a one-time password once. Copy it and pass it to the person yourself. They sign in with the address and that password, and they change it in Settings, General.
Two rules decide whether the button works:
- The owner grants the address as part of adding the person. A default install writes only your own address into
ALLOWED_SIGN_IN, so a colleague would be refused. The owner adding them writes the address to a second list in the database, and sign-in then accepts both lists.ALLOWED_SIGN_INstays the floor: the app only adds to the database list and never edits the environment. An admin who is not the owner still gets the old message naming the value to put indeploy/.env. PASSWORD_SIGN_IN="1"must be set, because the new person signs in with a password. Without it the button stays hidden. With Google, Microsoft or an identity provider a colleague on an allowed domain signs in alone and needs no account from you.
Your own session must be less than five minutes old, the same rule that guards a password change. Sign out and sign in again when the CRM asks for it.
The shell still works and does the same thing:
printf '%s' 'a long password' | docker compose exec -T api bun apps/api/scripts/create-owner.ts colleague@example.comThat script creates the account and sets the password. The person becomes a member on the first sign-in. Change the role in Settings, Members.
Removing a person is not in the app. Take the address off ALLOWED_SIGN_IN in deploy/.env and run docker compose up -d in the deploy folder. The next request locks the person out. The account, the member row and every record stay. For a person on a domain you keep, set a password only you know with the script above; that also ends every session they have.
An address the owner granted in the app sits in the database, not in deploy/.env, so the step above does not reach it. List and remove those addresses in the deploy folder:
docker compose exec -T api bun apps/api/scripts/sign-in-grants.ts list
docker compose exec -T api bun apps/api/scripts/sign-in-grants.ts revoke colleague@example.comThe next request locks that person out.