What's in the Box & Requirements
This edition supports one school with unlimited branches per installation. Customers may install it on Windows, Ubuntu, or Docker. For multi-school / SaaS licensing, contact the Kindie team.
📦 What's in the box
| Folder | What it is |
api/, config/, views/ | Sails.js (Node.js) backend + REST API |
react-backend/ | Web Admin SPA source (React + Vite + TypeScript) |
assets/web-admin-dist/ | The built Web Admin, served by the backend. It is not in the zip: Kindie builds it from react-backend/ the first time it starts (a few minutes) |
docker-compose.prod.yml, Caddyfile, Dockerfile | One-command Docker deployment for a real server — includes Caddy, which handles your domain name and HTTPS |
kindie.env | Your settings for Docker: address, time zone and email |
.env.example | All configuration, documented per variable |
start.bat, start.sh | Start files for your own computer: install, check and start Kindie in one step |
README.md, PRODUCTION-DOCKER.md, PRODUCTION-MANUAL.md | The installation steps from this guide in plain-text form: running Kindie on your own computer, going live on a server with Docker, and going live without Docker |
CUSTOMIZATION.md | Web Admin customization: web server, colors, language, logo and icons, loading screen — see "Customization" |
CHANGELOG.md | What changed in each version |
⚙️ Requirements
- Required A computer, VPS, or server you control. Shared PHP hosting is not supported.
- Required Node.js 20.x and MongoDB 8.0 for a manual installation.
- Required Docker + Docker Compose when using the Docker installation path instead.
- Required At least 8 GB of RAM and about 10 GB of free disk space — the first start downloads and builds everything.
- Optional Redis — only needed when several application instances share sessions, cache, or realtime connections.
- Recommended An SMTP account — without it, password-reset and system emails cannot be sent.
- Optional A Firebase project — only needed for mobile push notifications.
Server sizing: 8 GB of RAM and 10 GB of free disk are the minimum for installing; the project does not currently have a load-tested production sizing. Docker limits the application container to 2 GB and Node.js to a 1536 MB heap; a host running MongoDB, Redis, and the application needs additional memory and disk space for uploads, database growth, and backups. Do not treat 2 GB as a production sizing recommendation for the whole stack.
The release package includes the Web Admin as source (react-backend/), not as built files. Kindie builds it automatically the first time it starts (start file or Docker), so you only work with react-backend yourself if you are customizing it.
Before You Begin
This installation is self-service — you (or your IT contact) run it yourself, on whichever environment you already have or prefer. Docker Quick Start is the recommended path — fewest manual steps, least room for mistakes; the Manual Install path is documented in full too, for when Docker isn't an option. If you get stuck, the Kindie team is available to help — see "Contact & Support".
✅ Gather these before you start
| Status | Item | Why you need it |
| Recommended | Your Envato purchase code | Keep it handy for your records (Envato → Downloads → License certificate & purchase code) — the setup wizard does not currently ask for it during installation. |
| Required | A computer, VPS, or server you already control | Where Kindie will run — see "What's in the Box & Requirements" for what it needs installed. Going live on a VPS? Buy/rent it now and have its IP address, username and password ready — you'll need them to connect before installing anything. |
| Required | Admin access to that machine | sudo on Linux, Administrator on Windows, or the ability to install Docker. On a VPS: a way to connect over SSH — Windows needs PuTTY (download it now); macOS/Linux already has a Terminal that does this. |
| Required | The school's basic info | Name, logo, and the administrator account you want to create — the setup wizard asks for these directly |
| Recommended | An SMTP account | Without it, password-reset and system emails cannot be sent |
| Optional | A domain name (only if this will be a public website) | Needed for HTTPS — see "Production Deployment". Skip this if you're only trying Kindie out or running it on an internal office network. |
| Optional | A Firebase account (only if you want mobile push notifications) | Free to create — see "Application Configuration" |
🗺️ What happens next
Not sure which to pick? Start with Docker Quick Start — it's the fastest and simplest. Otherwise, use Manual Install (No Docker) and follow the bullet for your OS at each step. Both hand off to "First Run" for the same setup wizard regardless of which path you took.
Docker Quick Start Recommended
The fastest, simplest installation path, using Docker Compose — it installs Node.js and MongoDB for you inside containers (the production file adds Redis and Caddy). Works the same way on Windows, macOS and Linux. Haven't gathered what you need yet? See "Before You Begin" first.
You need: about 10 GB of free disk space, 8 GB of RAM, and an Internet connection for the first start. "The Kindie folder" below means the folder you downloaded/extracted Kindie into — you'll know it's the right one because it contains the file docker-compose.prod.yml.
🐋 Install Docker
Requires Docker Engine 24+ with the Compose plugin (the docker compose command, not the older separate docker-compose tool). Already have it? Verify with docker --version and docker compose version, then skip to "Get Kindie onto your server" below.
🖥️ Windows / 🍎 Mac: Docker Desktop
Step 1
- Go to docker.com/products/docker-desktop and download the installer for your computer — Windows: AMD64 (ARM64 only on Snapdragon laptops); Mac: Apple silicon (M1–M4) or Intel (not sure? Apple menu → About This Mac → Chip).
Step 2
- Windows: open
Docker Desktop Installer.exe from Downloads, click Yes if asked for permission, keep Use WSL 2 instead of Hyper-V ticked, click OK, and restart if asked.
- Mac: open the downloaded
Docker.dmg and drag Docker into Applications.
Step 3
- Open Docker Desktop (Start menu / Applications) and accept the terms — click Skip if it asks you to sign in. Wait until it shows Engine running (green) at the bottom left.
Step 4: Verify
- Open a new terminal (PowerShell on Windows, Terminal on Mac) and run:
docker --version
docker compose version

Docker and Compose answer with a version number
🐧 Ubuntu: Docker Engine
Step 1: Set up Docker's repository
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
Step 2: Install Docker Engine and the Compose plugin
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
In a hurry and just trying Kindie out (not production)? Docker's own one-line installer does the same thing: curl -fsSL https://get.docker.com | sudo sh — then still run Step 3 below.
Step 3: Run Docker without typing sudo every time, then restart
sudo usermod -aG docker $USER
Restart the computer (or log out and reconnect over SSH) for this to take effect.
Step 4: Verify
- Open a new terminal and run:
docker --version
docker compose version
Docker doesn't mean "nothing to prepare" — two things are still on you:
- Node.js, MongoDB, and Redis — you don't install any of these. They run inside containers that Docker Compose creates automatically (Node is baked into the Kindie image; MongoDB runs in its own container, and so does Redis in the production file).
- An SMTP account for outgoing email — Docker does not provide this. Create one with any provider (Mailgun, Gmail, your own mail server...) and enter it in the setup wizard, exactly like the manual-install path — this step is the same no matter how you install.
📤 Going live on a server
The package includes docker-compose.prod.yml, made for a real server. It bundles Caddy, which gets and renews your HTTPS certificate automatically and needs no manual setup beyond your domain name. Follow the six steps below.
Step 1: Point your domain at the server
Your server's IP address must be a public one. If it starts with 192.168., 10. or 172., it only works inside one office/home network — the Internet can't reach it and Kindie can't get its HTTPS certificate. Your hosting provider's dashboard shows the public IP.
- In your domain's DNS settings, add one A record: Name = the part before your domain (e.g.
kindie for kindie.myschool.com), Value = your server's IP address. Don't touch the records already there — your school's website/email use them.
- Using Cloudflare? Set Proxy status to DNS only (click the orange cloud so it turns grey) — with the orange cloud on, Kindie can't get its HTTPS certificate.
- ✅ Check with dnschecker.org: search your address, type A — it should show your server's IP. Can take minutes to a few hours; you can do the next steps while you wait.
Step 2: Open ports 80 and 443
In your hosting provider's dashboard, find the Firewall (sometimes called "Security group") page for your server and allow incoming TCP 80 and TCP 443 — Caddy needs both to get the HTTPS certificate. Keep TCP 22 (SSH) open too. No such page at your provider? The ports are usually already open.
Step 3: Set your address — on your own computer, before uploading
Open kindie.env (inside the unzipped Kindie folder, next to docker-compose.prod.yml) in a plain text editor and edit the BASE_URL line:
BASE_URL=https://school.example.com
Replace it with your own address, lowercase, no www., no trailing /, keeping https://:
BASE_URL=https://kindie.myschool.com
The same file also has a Time zone line (TZ=) and 6 commented-out Email lines (MAIL_…) — remove the # in front of each one you want to turn on now, or leave them for later (see "Application Configuration").
kindie.env is one setting per line, written NAME=value with no spaces around the = — don't add any.
Step 4: Copy the Kindie folder to the server
Use a free file-transfer program — connect with protocol SFTP, the IP/username/password your hosting provider gave you:
- Windows: WinSCP
- macOS / Linux: FileZilla (choose FileZilla Client; decline any other offered software)
Drag the whole Kindie folder into the server side, then rename it to exactly kindie (lowercase). ✅ Opening kindie on the server should show docker-compose.prod.yml directly inside it, not inside another folder. Prefer the terminal? scp -r kindie-web-v5.0.0 root@203.0.113.10:~/kindie does the same thing.
Step 5: Connect, check the server, install Docker
🐧 Linux (Ubuntu) — tested and supported for production
Connect over SSH — macOS/Linux: the built-in Terminal; Windows: PuTTY: ssh root@203.0.113.10. ✅ The start of the line you type on changes to something like root@my-server:~# — that confirms you're on the server, not your own computer, from here on. Leave this window open; you'll need it again in later steps.
Check the disk — Avail should read 20G or more:
df -h /
Too small? Enlarge the disk to 40 GB on your hosting provider's dashboard, restart the server, reconnect and check again.
Check the processor supports what MongoDB needs — should print a number other than 0:
grep -c avx /proc/cpuinfo
Prints 0? Ask your hosting provider: "Please set the CPU type of my server to 'host' (host-passthrough) so that AVX is available." On a VM this is usually a one-click setting; the server needs a full stop/start afterward.
Now install Docker:
sudo apt update && sudo apt install -y curl
curl -fsSL https://get.docker.com | sudo sh
✅ Check: sudo docker compose version prints a version number.
Every Docker command in this guide (and the rest of this page, for the VPS path) starts with sudo — without it you'll see permission denied. If a command asks for a password, it's your server password.
🖥️ Windows Server
Connect with Remote Desktop Connection (press Start, type mstsc, press Enter) using the IP/username/password your hosting provider gave you. Open PowerShell as Administrator once connected — you'll need it again in later steps.
Check the disk — Free should read 20 GB or more:
Get-PSDrive C
Too small? Enlarge the disk on your hosting provider's dashboard, restart the server, reconnect and check again.
The processor check (AVX, which MongoDB needs) is a feature of the virtual machine itself, independent of the host OS — Windows has no equivalent one-line command for it. Ask your hosting provider directly, or skip ahead and come back here only if the MongoDB container fails to start (see "Having trouble?" below).
Install Docker Desktop the same way as in "Install Docker" above — keep Use WSL 2 instead of Hyper-V ticked. Docker Desktop must stay running for Kindie to keep running; it is a desktop tool, not a background Windows service, so a server that restarts unattended needs Docker Desktop set to start automatically on login, and someone signed in (locally or via Remote Desktop) for it to keep running.
✅ Check, in the same PowerShell window:
docker compose version
Step 6: Start Kindie
First repeat the DNS check from Step 1 — don't continue until it shows your server's IP, or the HTTPS certificate will fail. Then, on the server:
🐧 Linux (Ubuntu)
cd ~/kindie
sudo docker compose -f docker-compose.prod.yml up -d --build
🖥️ Windows Server
cd C:\kindie
docker compose -f docker-compose.prod.yml up -d --build
No sudo on Windows — run it as-is in the PowerShell window from Step 5.
The first start downloads and prepares everything — it can take up to 15 minutes, keep the computer awake. It's finished when you can type again and the last lines look like this, every line with a ✔:
✔ Container kindie-redis-1 Started
✔ Container kindie-mongodb-1 Healthy
✔ Container kindie-kindie-1 Started
✔ Container kindie-caddy-1 Started
Window closed, or Connection closed? Reconnect, cd ~/kindie (Windows: cd C:\kindie), and run the same command again — it continues where it stopped. A line in red, or the word Error? See "Having trouble?" below.
Then, in a browser: open your address (https://kindie.myschool.com) — if it doesn't load yet, wait two minutes and refresh.

Setup wizard, step 1: School information
✅ You see the Kindie setup wizard with a padlock next to the address — no link, token or Envato code to enter. Kindie connects to its own database automatically and writes its security keys for you. Fill in your school, the administrator account, the settings and the school year, then click Finish and sign in.
Finish the wizard right away — until you do, anyone who opens your address can create the administrator account.
See "First Run" for what each step of the school-setup wizard asks for.
| Service | Reachable from | Notes |
| Caddy | Internet, ports 80/443 | Gets and renews the HTTPS certificate by itself, forwards everything to Kindie |
| Kindie application | Only from Caddy, not published directly. | Application and pre-built Web Admin interface |
| MongoDB | The kindie container, and this server itself (127.0.0.1) for admin tools like MongoDB Compass over an SSH tunnel. | Never reachable from the Internet — nothing to configure for that. |
| Redis | Only the kindie container | Keeps people signed in across restarts/updates |
Uploaded photos/files live in the Docker volume uploads — kept across restarts and updates, separate from the release folder.
🔁 Everyday use
Kindie keeps running by itself, also after the server restarts. Run these in the Kindie folder (cd ~/kindie first). Every command needs sudo (Windows: leave it out) and the -f docker-compose.prod.yml flag:
| To… | Command |
| Check it's running | sudo docker compose -f docker-compose.prod.yml ps |
| Stop | sudo docker compose -f docker-compose.prod.yml stop |
| Start again | sudo docker compose -f docker-compose.prod.yml up -d |
| See errors | sudo docker compose -f docker-compose.prod.yml logs -f kindie |
| Install a new version | See "Backup & Restore" → Updating Kindie |
📷 See it on screen
The screenshots were taken on a Windows computer without sudo and without the -f docker-compose.prod.yml flag. On a server, add both to every command, as written above.

Check it's running: both lines say Up

Stop Kindie

Start again: the command

Start again: the result

See errors: the command

See errors: the latest lines
Your data lives inside Docker (named volumes), not loose in the Kindie folder: stopping, restarting or updating keeps it; copying the Kindie folder does not copy it — see "Backup & Restore".
🩹 Having trouble?
While installing
| Problem | What to do |
permission denied while trying to connect to the docker API | The command is missing sudo at the start — add it and run again. |
no space left on device | The server's disk is too small — redo the disk check (Step 5): df -h /. After enlarging it, run sudo docker image prune -a (it removes every Docker image that is not in use on this server, including those of other projects), then start Kindie again (Step 6). |
dependency failed to start: container kindie-mongodb-1 is unhealthy | Redo the processor check (Step 5): grep -c avx /proc/cpuinfo. Prints 0? Ask your hosting provider to enable AVX (set CPU type to "host"), then start Kindie again. Otherwise send sudo docker compose -f docker-compose.prod.yml logs mongodb to support. |
docker: command not found, or a command shows your own computer's name instead of the server's | You're typing on your own computer, not the server — connect first (Step 5), then run the command there. |
cd ~/kindie says No such file or directory | The folder on the server has another name or is inside another folder — fix it in WinSCP/FileZilla (Step 4). |
port is already allocated / address already in use | Mentions 80 or 443: another web server is already on this machine — sudo systemctl stop nginx apache2. Mentions 27017: another MongoDB — sudo systemctl stop mongod. Then start Kindie again. |
An error mentioning kindie.env | not found: the file is missing from the Kindie folder — copy it back from the package you downloaded. Any other message (for example key cannot contain a space): a line in it is not written NAME=value — no spaces around the =, and every explanation line starts with #. |
An error mentioning yaml and a line number after editing the file | A space is missing/extra on that line, or the Tab key was used — compare with the lines around it. |
Opening Kindie
| Problem | What to do |
| The browser says the site is not secure, or can't be reached | The address doesn't point to the server yet, the IP isn't a public one, or ports 80/443 are closed — redo Steps 1–2 of "Going live", wait 10 minutes, refresh. Cloudflare: the cloud next to the record must be grey. |
| dnschecker shows the right IP, but the browser still can't open the page | Your computer remembers the old place — close the browser fully and reopen, try a private/incognito window, or try from a phone on mobile data. Clears within an hour by itself. |
| Signing in sends you back to the sign-in page | The BASE_URL in kindie.env doesn't exactly match the browser address (e.g. extra www., trailing /, or http://) — fix it, then sudo docker compose -f docker-compose.prod.yml up -d. |
| A plain Setup page appears instead of the wizard | Kindie started before MongoDB was ready. Run sudo docker compose -f docker-compose.prod.yml restart kindie, wait a minute, refresh. |
| The page doesn't open | Windows: check Docker Desktop shows Engine running. Still stuck? sudo docker compose -f docker-compose.prod.yml ps — if kindie says Restarting, run sudo docker compose -f docker-compose.prod.yml logs kindie and send the last lines to support. |
docker: command not found / 'docker' is not recognized (Windows) | Open Docker Desktop and wait for Engine running, then open a new terminal. (Linux: restart the computer after installing Docker.) |
| Windows: "WSL 2 installation is incomplete" | Right-click Start → Terminal (Admin), run wsl --update, then restart the computer. |
| Windows: "Virtualization must be enabled" | This is a setting in the computer's firmware (BIOS). Ask IT, or search "laptop brand enable virtualization". |
Using Kindie
| Problem | What to do |
| WinSCP/FileZilla says Permission denied copying the Firebase key | The .secrets folder wasn't prepared first — run sudo mkdir -p .secrets && sudo chown $USER .secrets on the server, then copy the file again. |
| Docker uses too much disk space | Run docker image prune -a (add sudo on production). It removes every unused Docker image on this computer, including images of other projects — they are downloaded again when needed. Never delete volumes — they hold your school's data. |
None of the steps above delete your data. To wipe everything and start over, or to uninstall completely, see "Backup & Restore" → Reset & Reinstall.
🔜 Now that it's running
💾 Back up your data (do this today)
Run in the Kindie folder. This backs up the database, uploaded photos/files, and chat attachments:
mkdir -p kindie-backup
sudo docker compose -f docker-compose.prod.yml exec mongodb mongodump --db=kindiedb --gzip --archive=/tmp/database.gz
sudo docker compose -f docker-compose.prod.yml cp mongodb:/tmp/database.gz kindie-backup/database.gz
sudo docker compose -f docker-compose.prod.yml cp kindie:/app/assets/uploads kindie-backup/photos
sudo docker compose -f docker-compose.prod.yml cp kindie:/app/.tmp/chat-attachments kindie-backup/chat-files
Also run sudo chown -R $USER kindie-backup afterward (the files are written as root), then download the kindie-backup folder to your own computer with WinSCP/FileZilla — a backup that only lives on the server is lost together with the server. For a scheduled nightly backup, see "Backup & Restore" → Automatic daily backup.
🔔 Push notifications (Firebase) — optional
Kindie works fully without this; the mobile app just won't receive push notifications until it's set up.
On the server (SSH), before copying the file — skipping this makes the file copy fail with "Permission denied": sudo mkdir -p .secrets && sudo chown $USER .secrets. Then in WinSCP/FileZilla, turn on hidden files (WinSCP: Ctrl+Alt+H; FileZilla: Server → Force showing hidden files), open .secrets and drag firebase-key.json in. Back on the server:
chmod 644 .secrets/firebase-key.json
sudo docker compose -f docker-compose.prod.yml restart kindie
The compose file already points GOOGLE_APPLICATION_CREDENTIALS at that file — nothing to edit elsewhere. Check it worked: sudo docker compose -f docker-compose.prod.yml exec kindie node scripts/check-firebase-credential.js. Full walkthrough, including how to get the key file from Firebase: "Application Configuration" → Push notifications (Firebase).
⚙️ Other settings
Email (SMTP), time zone, changing settings after install: see "Application Configuration" — for Docker, these live in kindie.env, a separate file from .env. Copy kindie.env and .secrets into the new folder whenever you update to a new version — together they hold all your settings.
Manual Install (No Docker)
Installing the platform services Kindie needs yourself — Node.js, MongoDB, and (optionally) Redis/Caddy — without Docker. Covers Windows, Linux (Ubuntu), and macOS/other Linux in one page; follow the bullet for your OS at each step. Prefer one command instead? See "Docker Quick Start" — it installs and manages all of this for you.
Requirements: for a real, public deployment, use a Linux VPS/dedicated server (Ubuntu 22.04/24.04 LTS recommended) or a Windows Server 2019/2022 VPS, reached remotely. A regular Windows/macOS/Linux PC also works for trying Kindie out or running it as an internal office server only. See "What's in the Box & Requirements" for sizing notes — there is no official minimum RAM/disk figure yet; a reasonable starting point on a Linux VPS is 2 vCPU, 4 GB RAM, 40 GB SSD, and MongoDB, logs, uploads and backups all grow disk usage over time, so monitor and expand as needed. Budget 1–2 hours the first time, mostly spent waiting for downloads and (on Linux, for a public domain) DNS to propagate.
🧩 How the pieces fit together
Browser ──HTTPS──▶ Caddy (ports 80/443) ──▶ Kindie (Node.js, port 1337) ──▶ MongoDB (port 27017)
public private, this server only private, this server only
For local or office use, Kindie is served directly on port 1337 without Caddy. Caddy is added only when Kindie needs a public domain name with HTTPS — on Linux or Windows, see "Production Deployment". Replace example values (your-school.com, 203.0.113.10, kindie-web-v5.0.0.zip, Asia/Ho_Chi_Minh) with your own everywhere you see them.
🔑 Connect to your server
On your own computer, skip this — go straight to "Install Node.js" below. Most production installs instead run on a rented VPS, which you reach remotely, not by sitting in front of it.
🔎 How to connect, by platform
- Linux VPS, from Windows
- Download PuTTY, open it, type the server's IP in "Host Name", click Open, then log in with the username/password from your provider.
- Linux VPS, from macOS / Linux
- Open the built-in Terminal app and run
ssh username@your-server-ip (e.g. ssh root@203.0.113.10), then enter the password when asked.
- Windows Server VPS
- Open Remote Desktop Connection (Start → type "Remote Desktop"), enter the server's IP address, click Connect, then sign in with the username (usually
Administrator) and password your provider gave you.
The first time you connect you'll likely see a warning (an unknown SSH host key, or a Remote Desktop certificate warning) — this is normal for a brand-new server; accept it to continue. From here on, everything below happens inside that session — on Windows it's a full desktop, so PowerShell, File Explorer and your browser all work exactly as on your own PC.
🔄 Prepare the system (Linux VPS)
Windows and macOS: skip to "Install Node.js" below.
sudo apt update
sudo apt upgrade -y
sudo apt install -y curl gnupg unzip ca-certificates
If a purple screen asks which services to restart, press Enter to accept the default. If the last line suggests a reboot, run sudo reboot, wait a minute, and connect again.
Time zone — Kindie records check-in times, attendance and schedules using the server's clock; a server left on UTC will record times several hours off:
sudo timedatectl set-timezone Asia/Ho_Chi_Minh
Not in Vietnam? Find your zone with timedatectl list-timezones | grep -i london (replace london with a city near you) and use that name instead.
Firewall — run these lines in this exact order. Allowing OpenSSH before enabling the firewall is what keeps you from locking yourself out of the server.
sudo ufw allow OpenSSH
sudo ufw --force enable
Only SSH is open for now — web ports (80/443) are opened later in "Production Deployment". Kindie's own port (1337) and MongoDB's port (27017) are never opened; they stay private to this server.
✅ Check (Linux VPS): timedatectl | grep "Time zone" shows the right zone, and sudo ufw status shows Status: active with OpenSSH ALLOW. Locked out anyway? Use your hosting provider's web console ("VNC"/"Console" in their dashboard) to log back in, then run sudo ufw allow OpenSSH.
🟢 Install Node.js
🖥️ Windows
Go to nodejs.org and download the 20.x LTS .msi installer (version 20.11.0 or newer), run it, and make sure "Add to PATH" is checked.
🐧 Linux (Ubuntu)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
🍎 macOS / other Linux
brew install node@20 (macOS), or your distro's Node 20 package / NodeSource (other Linux).
Do not use Node 16 — it's end-of-life. package.json requires Node ^20.11.0.
Verify on any OS — must print v20. followed by 11 or higher, e.g. v20.18.1:
node --version
npm --version

Windows: Node.js answers v20 and MongoDB is Running
🍃 Install MongoDB
🖥️ Windows
Download the .msi installer from the MongoDB Download Center (Community Server, choose Version 8.0, the newest 8.0.x), choose Complete, and check "Install MongoDB as a Service" — it then starts automatically with Windows. The installer also offers MongoDB Compass, an optional GUI to browse the database (not required).
🐧 Linux (Ubuntu), MongoDB 8.0
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu $(lsb_release -cs)/mongodb-org/8.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo systemctl enable --now mongod
Why 8.0: supported by MongoDB until October 2029 on both Ubuntu 22.04 and 24.04. Stay on 8.0 — $(lsb_release -cs) fills in your Ubuntu codename automatically, so routine updates never jump to a newer major version. Do not move to MongoDB 9.0 until a Kindie release says it supports it.
🍎 macOS / other Linux
macOS: brew tap mongodb/brew && brew install mongodb-community@8.0 (full guide). Other Linux: see MongoDB's official install docs for your distribution.
MongoDB should only ever listen on 127.0.0.1 (this same computer). Do not create a firewall rule for port 27017 or open it on your router/VPS firewall.
Verify on any OS, using mongosh (the MongoDB Shell — installed automatically with MongoDB above on Windows/Linux; on Ubuntu if it says "not found": sudo apt-get install -y mongodb-mongosh; on macOS: brew install mongosh):
mongosh --quiet --eval "db.runCommand({ ping: 1 })"
A reply containing ok: 1 means MongoDB is up and reachable. Linux: if instead systemctl status mongod shows the service failed with Illegal instruction or mentions AVX in the log, the server's CPU is too old for MongoDB 8 (common on very cheap/old VPS plans) — ask your hosting provider for a plan with a modern CPU.
Also install MongoDB Database Tools now (mongodump/mongorestore) — a separate package from the server above, needed later for "Backup & Restore": Windows/macOS/other Linux — download from the MongoDB Download Center (macOS: brew install mongodb-database-tools); Ubuntu: sudo apt-get install -y mongodb-database-tools.
🔴 Install Redis (optional)
Optional when a single Kindie instance runs on the server — without it, Kindie uses a file cache and in-memory sessions (restarting Kindie signs everyone out). Install it when several instances share sessions, cache, or realtime connections.
🖥️ Windows
There is no officially maintained native Redis build — use Memurai Developer (a Windows-native Redis-compatible server, check with memurai-cli ping), or run Redis inside WSL2 / a small Linux VM and point REDIS_URL at it.
🐧 Linux (Ubuntu)
sudo apt-get install -y redis-server
sudo systemctl enable redis-server
sudo systemctl start redis-server
Check with redis-cli ping (expect PONG).
🍎 macOS
brew install redis && brew services start redis, check with redis-cli ping.
🐳 Any OS, via Docker
docker run -d --name kindie-redis -p 6379:6379 redis:7
🌐 Install Caddy (public domain, any OS)
Skip this if you only need local/internal access (see "Local network access" below instead). To put your real domain name and HTTPS in front of Kindie, install and set up Caddy after Kindie is running, following "Production Deployment" — it covers every step on Linux and Windows: DNS, ports, installing Caddy, the Caddyfile, and running it as a service.
📤 Get Kindie onto the machine
🖥️ Windows
Server (Remote Desktop): open a browser inside the Remote Desktop window and download the zip from Envato directly, or copy it from your own computer with Ctrl+C / Ctrl+V through the Remote Desktop clipboard. Own PC: just use the copy you already downloaded.
Extract to a permanent folder such as C:\Kindie — do not run it from a temporary Downloads folder.
🐧 Linux VPS (SSH)
From your own computer (a second terminal window, not the SSH session): scp kindie-web-v5.0.0.zip root@203.0.113.10:~/ — or drag-and-drop with WinSCP (Windows) / FileZilla (any OS) over SFTP.
Back on the server, run Kindie under its own user, never root:
sudo adduser --system --group --home /home/kindie kindie
cd ~
unzip -q kindie-web-v*.zip -d kindie-unpack
sudo mv kindie-unpack/kindie-web-v* /opt/kindie
sudo chown -R kindie:kindie /opt/kindie
rmdir kindie-unpack
This kindie user is a service account — nobody can log in with it; it only exists to run Kindie with limited permissions.
✅ Check: ls /opt/kindie/app.js /opt/kindie/.env.example /opt/kindie/react-backend/package.json — all three paths listed, no "No such file or directory". If one is missing, the zip is incomplete or not an official package — download it again from Envato.
🍎 macOS / other Linux (own PC)
Just use the copy you already downloaded — nothing to transfer. Extract anywhere convenient, e.g. your home folder.
📦 Install dependencies & start Kindie
Step 1 & 2 (own computer / office PC): use the start file
- Windows: double-click
start.bat in the Kindie folder.
- macOS / Linux: open a terminal in the Kindie folder and run
./start.sh (first time: chmod +x start.sh).
The first run installs Kindie and builds the admin screens automatically (a few minutes) and then starts it — no separate npm install step. Later runs start straight away. The window may look idle for a while; it prints ... still working from time to time. Do not close it. If it does get interrupted (window closed, power or Internet lost), just run the start file again: it finishes the install.
Ubuntu: when Node.js or MongoDB is missing, ./start.sh says so and asks Install … now? — press Enter to install (or type n to skip) and type your password when asked.
Before it starts, the start file checks three things and prints one line for each:
| Line | Meaning |
| ✔ | All good. |
| ✗ | Kindie was not started. The lines below it say why — Node.js is not version 20, or another program is already using Kindie's address (port 1337). Fix it as the message says, then run the start file again. |
| ! | A warning: Kindie cannot reach its database (MongoDB). Kindie still starts. See "Having trouble?" below. |
If a step fails, or Kindie stops by itself, the window says what failed and the most likely cause in plain English. The same report is saved in the file kindie-error.log in the Kindie folder — attach that file when you write to support.
When the window shows ✔ Kindie is ready, open http://localhost:1337 (on Windows and macOS your browser opens it by itself).

Windows: start.bat prints three ✔ checks, then starts Kindie
🔎 Prefer to type the commands yourself?
The start file runs these commands in the Kindie folder. Everything before npm start is only needed the first time:
npm install
cd react-backend
npm ci
cd ..
npm run build:admin
npm start
Skipping npm run build:admin leaves the admin screens missing: the browser shows a "not found" error instead of the sign-in page.
Step 1 & 2 (Linux VPS / production, as the kindie service account)
On a VPS you're about to run as a systemd service (see "Running the Application"), install and do a first foreground start directly with npm instead of start.sh — start.sh is meant for double-click/interactive use, not a service account:
cd /opt/kindie
sudo -u kindie -H npm install --omit=dev
sudo -u kindie -H npm ci --prefix react-backend
sudo -u kindie -H npm run build:admin
sudo -u kindie -H npm start
The package ships the Web Admin as source, not as built files. The two middle commands build it into assets/web-admin-dist/ (a few minutes, about 4 GB of memory); without them the browser shows a "not found" error instead of the sign-in page.
npm error while building bcrypt or sharp? Install build tools and retry: Ubuntu — sudo apt install -y build-essential python3; Windows — install the "Desktop development with C++" workload from Visual Studio Build Tools; macOS — xcode-select --install. Then run npm install again. EPERM on Windows: run PowerShell as Administrator. EACCES on macOS/Linux: avoid sudo npm — use a Node version manager (e.g. nvm) instead.
With MongoDB already running at the default address (which you set up above), Kindie connects to it automatically on first start, writes its own .env and security keys, and prints a plain URL — no link token to copy. Couldn't reach MongoDB (wrong host/port, not started yet)? It shows a one-time Setup page instead, protected by a token printed in the same terminal output, where you enter the database connection by hand.
Starting with start.sh/start.bat or a foreground npm start only runs Kindie while that window stays open — see "Running the Application" to keep it running permanently as a background service (systemd on Linux, WinSW on Windows).
The backend serves the built admin interface itself (assets/web-admin-dist/) on the same port — no separate frontend server required.
✅ Check (any OS, in a second terminal window): curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:1337/ prints 200 while Kindie is running.
Step 3: Complete the wizard
- Open the URL printed in the terminal. Fill in your school, the administrator account, the settings and the school year. See "First Run" for what each step asks for.
Optional: rebuild the admin interface after customizing it
npm ci --prefix react-backend
npm run build:admin
# output goes to assets/web-admin-dist
The single build mode matters: it leaves the API URL empty so the admin talks to your own server, and forces the interface to English.
Automated / unattended deploys: see "Application Configuration" → "Unattended / scripted deploys" for the SKIP_INSTALLER option.
🩹 Having trouble?
| Symptom | Cause and fix |
| Locked out of SSH after turning on the firewall | OpenSSH was not allowed first. Use your hosting provider's web console ("VNC"/"Console") to log in, then run sudo ufw allow OpenSSH. |
| MongoDB will not start; the log mentions Illegal instruction or AVX | The server's CPU lacks features MongoDB 8 needs (common on very cheap/old VPS plans). Ask your hosting provider for a plan with a modern CPU. |
mongosh: ECONNREFUSED 127.0.0.1:27017 | MongoDB is not running: Linux — sudo systemctl start mongod; Windows — Start-Service MongoDB (PowerShell, as Administrator). |
node -v shows an old version (Linux) | Ubuntu's own Node.js got installed instead of NodeSource's. Repeat "Install Node.js" — the NodeSource version replaces it. |
Service shows failed or keeps restarting (Linux systemd) | Read the log: sudo journalctl -u kindie -n 50 --no-pager. Permission denied means ownership was lost: sudo chown -R kindie:kindie /opt/kindie. |
The start file shows ✗ Kindie needs Node.js 20 | Your Node.js is another version; a newer one does not work either. Windows: open Settings → Apps, uninstall Node.js, then install version 20. macOS / Linux with nvm: nvm install 20 && nvm use 20. Ubuntu without nvm: the start file offers to install version 20 and replaces the Node.js already there. Then open a new terminal, run node -v (it must start with v20.) and run the start file again. |
The window shows ✗ Kindie stopped because of an error | Read the lines under it: they name the most likely cause and what to do. If it says files are missing, run the start file again — it reinstalls them. Still stuck? Send the file kindie-error.log (in the Kindie folder) to support. |
| The first start was interrupted (window closed, power or Internet lost) | Run the start file again. It notices the unfinished install and completes it. |
out of memory while the admin screens are being built | Close other programs (browser tabs, office programs) and run the start file again. This one-time step needs about 4 GB of memory. |
The start file shows ! Cannot reach MongoDB, or a plain Setup page appears instead of the wizard | Kindie could not reach MongoDB at the default address. Confirm it's running (see "Install MongoDB" above) and restart Kindie — or fill in the database address shown on that page. |
| The terminal prints nothing / Kindie exits immediately | Kindie is not running, or setup was already completed. If .env / .install.lock already exist, see "Backup & Restore" → Reset & Reinstall to start over. |
✗ Port 1337 is already in use / port is already allocated / EADDRINUSE | Something else is using port 1337 — often a second copy of Kindie, or Kindie running with Docker (run sudo docker compose -f docker-compose.prod.yml stop first). Find and stop it, or change PORT in .env (after the wizard writes it) and restart. |
📷 What these errors look like
Examples from the start file on Windows.

Node.js is not version 20: the start file refuses to start

Kindie stopped because of an error: the window names the likely cause
🔥 Local network access (internal use only)
By default only this computer can open Kindie. If you only need other computers on the same office network to reach it — not a public domain — open port 1337 through the OS firewall, then share this computer's local IP address with staff, who open http://THAT-IP:1337 in their browser.
- Windows (PowerShell, as Administrator):
New-NetFirewallRule -DisplayName "Kindie" -Direction Inbound -Protocol TCP -LocalPort 1337 -Action Allow — find the IP with ipconfig ("IPv4 Address").
- Linux:
sudo ufw allow from 192.168.0.0/16 to any port 1337 (adjust the range to your LAN) — find the IP with ip addr.
For a real public website with a domain name and HTTPS, skip this and follow "Production Deployment" instead.
🔜 Now that it's running
💾 Back up your data (do this today)
- Linux:
mongodump --host 127.0.0.1 --port 27017 --db kindiedb --out ./backup/kindiedb, plus copy .env and assets/uploads. For a script that does all of this automatically every night, see "Backup & Restore" → Automatic daily backup.
- Windows: same idea — see "Backup & Restore" for the downloadable
backup-kindie.ps1.example script and Task Scheduler setup.
Keep a copy somewhere other than this server.
🔔 Push notifications (Firebase) — optional
Kindie works fully without this; the mobile app just won't receive push notifications until it's set up. Create a Firebase service-account key, point GOOGLE_APPLICATION_CREDENTIALS in .env at it, then verify with node scripts/check-firebase-credential.js. Full walkthrough: "Application Configuration" → Push notifications (Firebase).
⚙️ Other settings
SMTP email, changing settings after install, unattended/scripted deploys: see "Application Configuration". A real domain name + HTTPS instead of a bare IP address: see "Production Deployment".
First Run
What to do the first time you open Kindie after installing: the setup wizard, then your first day as school administrator. This is the Web Admin wizard — the same one whether you installed with Docker or manually.
🔓 Open the wizard
Open your Kindie address in a browser — https://your-school.com on a server, or http://localhost:1337 on your own computer. On a fresh install the setup wizard opens by itself (you can also go to /installation). There is no link, token or purchase code to enter.
- The wizard has 4 steps. Nothing is saved on the server until the very last button, Complete Setup & Launch, so you can go back and change anything before then.
- Your answers are kept while the browser tab stays open — if you refresh the page you continue where you were, but you must type the password again. Closing the tab starts over.
Finish the wizard right away — until you do, anyone who opens your address can create the administrator account.
Have these ready
- School code, name, email, phone and address (tax code and logo if you have them).
- The email address and password you want for the administrator account.
- The start and end dates of the school year you are opening.
- Decide how invoices and payments will work (cash, bank transfer, PayPal, Stripe) — Step 3 asks, and you can change it later.
🚀 Setting up your school: the 4 steps
Step 1: School Information
The basic profile of your school.
| Field | Required | What to enter |
| School Code | Yes | A short code for your school, for example HM. Kindie turns it into capital letters and removes spaces and accents. It is also used inside class codes. |
| School Name | Yes | The full name of the school. |
| E-mail | Yes | The school's contact email, a valid address. |
| Phone | Yes | Digits, with an optional + at the start; spaces, dots, dashes and brackets are allowed (6–20 characters), e.g. +84 28 1234 5678. |
| Address | Yes | The school's address. |
| Tax Code / VAT Number | No | Leave empty if your country has no such number. |
| School Logo | No | PNG or JPG. Best fit is a 4:3 picture, for example 600×450 px. |
Step 2: School Admin Account
The login you will use from now on. It is the highest account in the school, with full access including finance and student records.
| Field | Required | What to enter |
| Username | Yes | The name you sign in with. |
| Display Name | Yes | How your name appears in Kindie, for example Anna Smith. |
| Name Display Style | No | Given Name & Family Name or Family Name & Given Name — the order your name is shown and split in. Pick the one your country uses. |
| Email Address | Yes | A valid address, not already used by another account. Password-reset emails go here. |
| Password | Yes | At least 6 characters — longer is safer. |
| Confirm Password | Yes | The same password again. |
Write down the email and password somewhere safe before you continue — there is no "forgot password" email until you set up email in "Application Configuration".

Setup wizard, step 2: School admin account
Step 3: System Settings
Three tabs — General, Numbering and App. These are the same panels as in the Web Admin's Settings page, so everything here can be changed later; the defaults are fine for a first start. Required fields are marked, and if one is missing Kindie jumps to its tab.
General tab
| Setting | What it does |
| Automatic Sample Data | Off by default. Turn it on only if you want a populated demo to explore: it creates a sample branch and school year, 2 classes, 2 students, 2 teachers, 2 parents, 2 subjects and 2 foods. The demo teacher and parent accounts use the password 123456 — delete them before you put real data on the system. |
| Display & Format | Name display style, date format (default DD/MM/YYYY), rows per page (default 20). The school's default language is English in this edition and cannot be changed here. |
| Payment Settings | The payment methods parents can use: Cash (with instructions shown to parents), Bank Transfer (choose the bank, account number and account name — parents get a payment QR code), PayPal (Client ID and Secret) and Stripe (Publishable and Secret key). PayPal and Stripe each have an Environment: Sandbox (testing) or Production (live). Use Sandbox until you have tested a payment. |
| Tuition & invoicing | Default tax for new invoice lines (0), leave notice in days (1), reminder before the due date in days (3), maximum adjustment (100000), the day of the month enrichment fees are cut off (15, from 1 to 28), and whether enrichment fees are merged into the monthly tuition invoice (default) or issued as a separate invoice. |
| Other Settings | Weekend days, school day start (06:00) and end (18:00), emails that receive new-application alerts, how long before an application counts as waiting too long (3 days) and how often to alert (15 minutes), and three switches: Enable Shuttle Bus on App, Allow Modify Attendance on Web and Enable Authorized Person (authorized pick-up people). |
Numbering tab
How Kindie numbers invoices, expenses, classes and students. Each type has Automatic numbering, a Prefix (up to 12 characters: letters, digits, - _ / .), the Number of digits and the Next number, with a live preview.
| Type | Default |
| Invoice | On — prefix INV-, format YYMMDD-SEQ, 4 digits |
| Expense | On — prefix EXP-, format YYMM-SEQ, 4 digits |
| Class | Off — when on, codes look like HM-CS1-26-M1-0001 (school – branch – year – group – number) |
| Student | Off — when on, format BRANCH-YY-SEQ, 4 digits |
With automatic numbering off, codes are typed in by hand or generated randomly.
App tab
- App information — App Name (required) and description, plus the contact details shown to families: technical and sales email, hotline, technical and sales phone.
- Notification Settings — switches for news, album, menu, schedule, day-off and event notifications; all off by default.
- Absence Note — the text parents see on the leave-request screen of the app.

Setup wizard, step 3: System settings
Step 4: Academic Session
Your first school year.
| Field | Required | What to enter |
| Session Code | Yes | A short code, for example 2026 - 2027. |
| Session Title | Yes | The name shown in Kindie, for example School Year 2026 - 2027. |
| Session Duration | Yes | Start Date and End Date. The end date must be after the start date, and dates in the past cannot be picked. |
Terms and semesters can be added or changed later — see User Guide → School Administration → Sessions.
Finish
Click Complete Setup & Launch. Kindie then creates, in one go: your school, the administrator account, a first branch named after your school (code MAIN), the default roles Accountant and Content Staff, your settings and numbering formats, and the school year. You land on the sign-in page — sign in with the administrator account you just created.
Something went wrong at the last step? Click the button again — it continues from where it stopped.
Once you finish the wizard, opening
/installation again simply redirects to the login page — it does not run twice. To start over from the first step, see "Backup & Restore" →
Reset & Reinstall.

Setup wizard, step 4: Academic session (your first school year)
✅ Verify your install
Five quick checks, while everything is still fresh — do them now rather than discovering a problem later with real data on the system.
1. Sign in
- Sign in with the administrator account you just created (Step 2) and check that you see the Dashboard.

The sign-in page

The Dashboard after the first sign-in
2. Upload a picture
- Upload any photo — in an album, or as a profile picture — and check that it displays. This proves uploads are saved and served correctly.
3. Check the time
- Record something with a time on it (e.g. an attendance check-in) and confirm it shows the school's local time. Off by several hours? Docker: edit the
TZ= line in kindie.env and apply it (sudo docker compose -f docker-compose.prod.yml up -d) — the container has its own time zone, independent of the host OS. Manual Install: fix it at the OS level instead (see "Manual Install (No Docker)" → Prepare the system) and restart the application.
4. Survive a restart
- Restart the server (or just the container for Docker) and reload the website after a minute. If it doesn't come back by itself, revisit "Running the Application" — the service/supervisor step is what makes this automatic.
5. Set up backups
- Do this today, before real data goes in — see "Backup & Restore".
🧭 Your first day as school administrator
The wizard only creates the school. These are the usual next steps, in a sensible order — each one is explained in the User Guide tab.
- Sign in and look around. Use the administrator account from Step 2 — see User Guide → Login and Dashboard.
- Review your settings. Payment methods, invoicing rules and numbering formats from Step 3 can be changed any time in the Web Admin's Settings — see User Guide → System & Settings.
- Set up your branches. The wizard created one branch named after your school; rename it or add more — see User Guide → School Administration → Branch Management.
- Check the school year. Confirm the year from Step 4 and add terms if you use them — User Guide → School Administration → Sessions.
- Create classes. Add your classes (and class groups) before importing children — User Guide → School Administration → Classes and Group Class.
- Add staff accounts. Create accounts for teachers, accountants and content staff and give each the right role (Accountant and Content Staff already exist) — User Guide → School Administration → Users.
- Import students and parents. Go to People → Import, download the Excel template, fill it in, then upload. The template includes Height, Weight, Blood Group and Allergy columns — filling in Allergy is strongly recommended: the Daily Feed blocks meal actions that conflict with a child's recorded allergies. See User Guide → Profiles & Class Transfer.
- Set up fees. Create your fee items and invoices — User Guide → Finance & Tuition.
- Turn on email. Password-reset and invoice emails only work after you set up an email account — see "Application Configuration" → SMTP. Do this before inviting parents.
- Invite families. Share the Kindie App with parents, teachers and drivers — see "Mobile App Setup" and User Guide → Mobile App → Access & Login.
- Back up. Do it today, before real data goes in — see "Backup & Restore".
🎬 Video demo
A full walkthrough of the steps above, start to finish.
Application Configuration
How configuration actually gets into .env, and how to change it later.
📝 First install: use the wizard, not a text editor
.env is written for you by the setup wizard on first start (see "First Run") — you do not hand-edit .env.example yourself for a normal install, whether you're using Docker or running manually.
🔧 Changing configuration after install
To change a setting later (SMTP, Redis, mail sender, etc.), edit .env directly and restart the application — the wizard does not run again once .install.lock exists. .env.example documents every variable the application reads, one by one.
🤖 Unattended / scripted deploys
For CI or infrastructure-as-code setups where nobody opens a browser to run the wizard, set SKIP_INSTALLER=true and provide every required variable yourself (Docker secrets, Kubernetes secrets, CI variables...). The minimum for the app to start:
NODE_ENV=production
EDITION=single
SKIP_INSTALLER=true
BASE_URL=https://your-domain.com
MONGO_HOST=127.0.0.1
MONGO_DB_NAME=kindiedb
TOKEN_SECRET=<48+ random characters>
SESSION_SECRET=<48+ random characters>
The app validates this at startup and tells you exactly what's missing, instead of failing silently.
🔥 Push notifications (Firebase)
This entire section is optional — skip it if you don't need mobile push notifications yet. The app starts fine without it.
Step 1: Use the same Firebase project as the mobile app
- This must be the same Firebase project the Kindie mobile app already uses — not a new one. Push notifications only work when the server and the app point at the same project; a freshly created project won't be linked to the app and notifications will silently never arrive.
- No Firebase project yet for the mobile app? Create it following the mobile app's own setup guide first, then come back here — see "Mobile App Setup".
- Go to the Firebase Console, sign in with the Google account that owns that project, and open it.
Step 2: Generate the service-account key
- Inside the project, click the ⚙️ gear icon → Project settings → Service accounts tab.
- Click Generate new private key — this downloads a
.json file. Keep it safe; anyone with this file can send push notifications as your app.
Step 3: Add it to Kindie
Three ways to do this — pick the one matching how you installed:
- Docker Quick Start: in the Kindie folder, create a
.secrets folder and put the key file in it, renamed to firebase-key.json — docker-compose.prod.yml already points GOOGLE_APPLICATION_CREDENTIALS at /app/.secrets/firebase-key.json for you, nothing to edit in .env:
mkdir -p .secrets
mv ~/Downloads/your-project-firebase-adminsdk.json .secrets/firebase-key.json
sudo docker compose -f docker-compose.prod.yml restart kindie
On the server, do this over SSH + WinSCP/FileZilla — see "Docker Quick Start" → Now that it's running for the full walkthrough. Linux host: also run chmod 644 .secrets/firebase-key.json so the container user can read it.
- Manual Install (No Docker): copy the key file onto the server (e.g.
scp your-key.json root@203.0.113.10:~/firebase-key.json), then move it into place and lock it down so only Kindie can read it:
sudo mkdir -p /etc/kindie
sudo mv ~/firebase-key.json /etc/kindie/firebase-service-account.json
sudo chown kindie:kindie /etc/kindie/firebase-service-account.json
sudo chmod 600 /etc/kindie/firebase-service-account.json
Then set this line in .env: GOOGLE_APPLICATION_CREDENTIALS=/etc/kindie/firebase-service-account.json
- Alternative (any install, including Docker): open the downloaded
.json file in a text editor, copy its entire contents, and paste it as one line into FIREBASE_SERVICE_ACCOUNT_JSON in .env.
Restart the application afterward (Manual Install: sudo systemctl restart kindie or restart the npm start process; Docker: sudo docker compose -f docker-compose.prod.yml restart kindie).
Step 4: Check it works
node scripts/check-firebase-credential.js
Docker: sudo docker compose -f docker-compose.prod.yml exec kindie node scripts/check-firebase-credential.js. You should see your project ID and two lines starting with ✔.
Without this variable the app still starts, but push notifications never fire — see "Troubleshooting". Never share this .json file or paste it into a support ticket/screenshot.
📧 Customer-created services
Configure only the services you use
- SMTP
- Create an SMTP account with your email provider. Without valid SMTP settings, password-reset and invoice emails will not be delivered. Edit the 6
MAIL_… lines — Docker: in kindie.env; Manual Install: in .env (📄 download mail-smtp.env.example for a filled-in reference either way). After saving, apply and test: Docker — sudo docker compose -f docker-compose.prod.yml exec kindie node scripts/check-mail-transport.js; Manual Install — node scripts/check-mail-transport.js. It checks the connection, TLS and login without sending a real email or printing your password — two lines starting with ✔ mean it's working. Then confirm end-to-end with Forgot password on the sign-in page.
- Firebase
- Create a Firebase project and service account only when mobile push notifications are required. Firebase is not required for the application to start.
- Redis
- Use the Redis URL when running more than one Kindie application instance or when shared cache/session storage is required — otherwise leave it blank in the setup wizard and the app uses an in-memory cache. The Docker Compose file always runs a Redis container. Note:
.env.example's own sample value shows TYPE_CACHE=redis as an illustration of the Redis option — it is not a requirement, and the setup wizard writes the correct value for your actual choice.
- File storage
- Single-server installations can store files on the server disk. Back up uploads together with the database.
- Upload size limits
- Default: pictures up to 5 MB, other files up to 10 MB (chat allows larger pictures/videos). To raise them, add
UPLOAD_MAX_IMAGE_MB and/or UPLOAD_MAX_FILE_MB (value in MB) to .env — Docker: the environment: block of docker-compose.prod.yml — and restart. If you also put Caddy or another reverse proxy in front, raise its own upload limit to match (see "Production Deployment").
Never put real passwords, service-account JSON, or security keys in screenshots, support tickets, or public repositories.
Running the Application
Starting the app for development, and keeping it running in production.
▶️ Start or test the application
npm install
npm start
The default application port is 1337. Stop a foreground test run with Ctrl+C.
🔁 Production (keep it running)
Run Kindie under a process supervisor such as a Windows Service, systemd, or Docker Compose. The supervisor must restart the application after a server restart and keep the application folder and .env file unchanged.
Do not use sails lift or forever start as the normal customer deployment command. The release package is started with npm start.
Using Docker? You can skip the rest of this section — restart: unless-stopped in docker-compose.prod.yml already keeps the application running and restarts it after a reboot, automatically.
🐧 Linux: systemd
Step 1: Create the service
Copy the whole box and paste it in — it writes /etc/systemd/system/kindie.service in one go. Edit WorkingDirectory first if Kindie isn't at /opt/kindie. 📄 Download kindie.service.example if you'd rather edit it as a file first.
sudo tee /etc/systemd/system/kindie.service > /dev/null <<'EOF'
[Unit]
Description=Kindie
After=network-online.target mongod.service
Wants=network-online.target mongod.service
[Service]
Type=simple
User=kindie
Group=kindie
WorkingDirectory=/opt/kindie
ExecStart=/usr/bin/node lib/start.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
🔎 What the important lines mean
- After / Wants mongod
- Waits for MongoDB to start before starting Kindie.
- User=kindie
- Runs Kindie with the limited service account created in "Manual Install (No Docker)", never as root.
- lib/start.js
- The same entry point
npm start uses — it switches Kindie into production mode on its own.
- Restart=on-failure
- If Kindie crashes, systemd starts it again after 5 seconds.
Step 2: Start it
sudo systemctl daemon-reload
sudo systemctl enable --now kindie
sudo systemctl status kindie --no-pager
Shows active (running) in green.
🖥️ Windows: run as a service (WinSW)
Windows has no built-in way to turn an arbitrary command into a service. sc.exe create (built into Windows) looks like it works but doesn't — plain console programs like node.exe never report back to the Service Control Manager, so the service gets stuck in START_PENDING forever and can't be stopped cleanly. Use WinSW (Windows Service Wrapper) instead — it's free, and built specifically to wrap a plain executable correctly.
Step 1
- Download the latest
WinSW-x64.exe from the link above, copy it into your Kindie folder, and rename it to kindie-service.exe.
Step 2
- Next to it, create a file with the same name but ending in
.xml — kindie-service.xml — containing:
<service>
<id>kindie</id>
<name>Kindie</name>
<description>Kindie Web Admin backend</description>
<executable>C:\Program Files\nodejs\node.exe</executable>
<arguments>lib\start.js</arguments>
<workingdirectory>C:\Kindie</workingdirectory>
<log mode="roll"></log>
</service>
- Adjust
<executable> if Node.js isn't installed there (check with where node), and <workingdirectory> to your actual Kindie folder.
Step 3
- Open PowerShell as Administrator in that folder:
.\kindie-service.exe install
.\kindie-service.exe start
✅ Check: sc.exe query kindie shows STATE: RUNNING with STOPPABLE in the flags (not START_PENDING/NOT_STOPPABLE — that combination means the service isn't really working). Kindie now starts automatically with Windows, with no terminal window needed; manage it from services.msc like any other Windows service.
📋 Viewing logs
| Environment | Command / location |
| Linux, systemd | sudo journalctl -u kindie -f (add --since "1 hour ago" to narrow it down) |
| Docker Compose | sudo docker compose -f docker-compose.prod.yml logs -f kindie |
| Windows, WinSW | The <log mode="roll"> line in the .xml config writes rolling log files next to kindie-service.exe — look for kindie-service.out.log / .err.log. |
| Foreground test run (any OS) | Printed directly in the terminal where you ran npm start |
Production Deployment
Step-by-step setup of the web server that puts your own domain name and HTTPS in front of Kindie. It uses Caddy — the same free tool on Linux and Windows — which gets and renews the HTTPS certificate by itself, so there is no separate certificate step.
Installed with Docker Quick Start? Caddy is already bundled in docker-compose.prod.yml: you only point your domain at the server, open ports 80 and 443, and set your address in kindie.env — see "Docker Quick Start" → Going live on a VPS. The steps below are for Manual Install (No Docker).
📄 Download Caddyfile.example — the Caddy configuration used below, as a ready-to-edit file.
🧭 How it fits together
Browser ──HTTPS──▶ Caddy (ports 80/443) ──▶ Kindie (Node.js, port 1337) ──▶ MongoDB (port 27017)
public private, this server only private, this server only
Caddy is the only part the Internet can reach. It accepts secure connections for your domain and passes them to Kindie, which stays private on the same server.
Before you start
- Kindie is installed and running on this server ("Manual Install (No Docker)") and answers at
http://127.0.0.1:1337.
- You own a domain name (for example
your-school.com) and can log in to where its DNS is managed.
- The server has a public IP address, and you can use
sudo (Linux) or an Administrator PowerShell (Windows).
🌐 Step 1: Point your domain at the server
- Find your server's public IP address — on the VPS, run
curl -4 ifconfig.me (Linux) or check your hosting provider's dashboard.
- Log in to wherever you bought/manage the domain (the registrar, or a DNS service such as Cloudflare), find the DNS or DNS records section, and add an A record: Host/Name
@ (or a subdomain like app), Type A, Value = your server's IP address.
- Using Cloudflare? Set the record's Proxy status to DNS only (grey cloud) until Caddy has its certificate — with the orange cloud on, it cannot get one.
DNS changes can take anywhere from a few minutes up to 24–48 hours to take effect everywhere. Check with
dnschecker.org (or Linux:
getent hosts your-school.com) — once it returns your server's IP, continue.
🔓 Step 2: Open ports 80 and 443
Caddy needs both ports open to the Internet to get the HTTPS certificate and to serve your users — keep ports 1337 (Kindie), 27017 (MongoDB) and 6379 (Redis) private.
🐧 Linux
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status
Also open TCP 80 and TCP 443 in your hosting provider's dashboard if it has its own Firewall (or "Security group") page.
🖥️ Windows
First open TCP 80 and TCP 443 in your hosting provider's dashboard, on its Firewall (or "Security group") page, if it has one. Then allow them in Windows Firewall — PowerShell as Administrator:
New-NetFirewallRule -DisplayName "Caddy web server" -Direction Inbound -Protocol TCP -LocalPort 80,443 -Action Allow
📥 Step 3: Install Caddy
🐧 Linux (Ubuntu)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
✅ Check: caddy version prints a version number. Caddy installs itself as a service and starts automatically — nothing else to enable.
🖥️ Windows
- Go to caddyserver.com/download, choose platform Windows (amd64) and download.
- Create a permanent folder
C:\Caddy, and put the downloaded file in it, renamed to caddy.exe.
✅ Check — PowerShell:
cd C:\Caddy
.\caddy.exe version
📝 Step 4: Create the Caddyfile
The Caddyfile is Caddy's settings file. Replace your-school.com with your own domain — only the name, without https://.
🐧 Linux
- Paste this whole box in — it writes
/etc/caddy/Caddyfile in one go:
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
your-school.com {
request_body {
max_size 100MB
}
reverse_proxy 127.0.0.1:1337
}
EOF
🖥️ Windows
- Open Notepad, paste the text below, and save it as
C:\Caddy\Caddyfile. In the Save dialog set Save as type to All files — otherwise Windows saves it as Caddyfile.txt and Caddy will not find it.
your-school.com {
request_body {
max_size 100MB
}
reverse_proxy 127.0.0.1:1337
}
The request_body { max_size 100MB } block is set comfortably above Kindie's own upload limits (pictures 5 MB, other files 10 MB by default; chat allows larger pictures/videos) — without it, large uploads could be rejected by Caddy before they even reach the app. See "Application Configuration" to raise Kindie's own limits.
▶️ Step 5: Start Caddy
🐧 Linux
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
✅ The second command shows active (running). Caddy now starts by itself whenever the server restarts.
🖥️ Windows
A. Try it first — PowerShell in C:\Caddy. The first command checks the Caddyfile for typing mistakes, the second starts Caddy in this window:
.\caddy.exe validate --config Caddyfile
.\caddy.exe run --config Caddyfile
Open https://your-school.com (see Step 6). It works? Press Ctrl + C in the PowerShell window to stop Caddy, then continue with B so it runs without a window.
B. Run it as a Windows service — Windows has no built-in way to turn a program like caddy.exe into a service (sc.exe create gets stuck in START_PENDING), so use WinSW, the same tool used for Kindie in "Running the Application":
- Download
WinSW-x64.exe, copy it into C:\Caddy and rename it to caddy-service.exe.
- Next to it, create
caddy-service.xml (same name, ending in .xml) containing:
<service>
<id>caddy</id>
<name>Caddy</name>
<description>Caddy web server — HTTPS for Kindie</description>
<executable>C:\Caddy\caddy.exe</executable>
<arguments>run --config C:\Caddy\Caddyfile</arguments>
<workingdirectory>C:\Caddy</workingdirectory>
<log mode="roll"></log>
</service>
- PowerShell as Administrator in
C:\Caddy:
.\caddy-service.exe install
.\caddy-service.exe start
✅ sc.exe query caddy shows STATE: RUNNING with STOPPABLE in the flags. Caddy now starts with Windows; manage it from services.msc.
✅ Step 6: Check it works and set Kindie's address
- Open
https://your-school.com in a browser. You should see Kindie (the sign-in page, or the setup wizard on a fresh install) with a padlock next to the address. The first visit can take a few seconds while Caddy gets the certificate.
- Tell Kindie its public address: open the
.env file in the Kindie folder, set BASE_URL to exactly your address — lowercase, with https://, no www. unless you use it, no / at the end — and restart Kindie:
BASE_URL=https://your-school.com
- Linux:
sudo systemctl restart kindie · Windows: Restart-Service kindie (PowerShell as Administrator).
Every link in outgoing email is built from BASE_URL, and signing in only works when it matches the address in the browser. From now on, open Kindie at https://your-school.com — not http://SERVER_IP:1337.
Kindie trusts the headers Caddy adds automatically (including X-Forwarded-Proto) to know it's being served over HTTPS — without it, secure cookies and links built from BASE_URL can misbehave. Nothing to configure for this; Caddy sets them by default.
🔧 Later: change the Caddyfile or update Caddy
| To… | Linux | Windows (PowerShell as Administrator) |
| Apply a changed Caddyfile | sudo systemctl reload caddy | Restart-Service caddy |
| Stop / start Caddy | sudo systemctl stop caddy / start caddy | Stop-Service caddy / Start-Service caddy |
| Update Caddy | sudo apt update && sudo apt install --only-upgrade caddy | Stop-Service caddy, replace C:\Caddy\caddy.exe with the new download, Start-Service caddy |
| See what Caddy is doing | sudo journalctl -u caddy -f | Log files next to caddy-service.exe (caddy-service.out.log, caddy-service.err.log) |
🩹 If something's not right
| Symptom | Cause and fix |
| The browser says the site is not secure, or can't be reached | The domain doesn't point to this server yet, or ports 80/443 are closed (Step 1–2) — wait a few minutes and refresh. Test from outside your own network, for example from a phone on mobile data. |
Caddy cannot get a certificate (its log mentions acme, challenge or obtaining certificate) | Let's Encrypt must reach your server on ports 80 and 443 using your domain. Check that the DNS record shows this server's IP (Step 1), that both ports are open (Step 2), and that Cloudflare is on DNS only. Caddy retries by itself — fix the cause, then reload/restart Caddy. |
port is already allocated / Caddy won't start | Another web server already uses ports 80/443 on this machine. Linux: sudo systemctl stop nginx apache2 (then sudo systemctl disable nginx apache2 so they stay off). Windows (IIS): Stop-Service W3SVC and Set-Service W3SVC -StartupType Disabled. Then start Caddy again. |
Windows: Caddy says it cannot find a Caddyfile, or validate reports a syntax error | The file was saved as Caddyfile.txt — rename it to Caddyfile (no extension). Syntax errors name the line: compare it with Step 4, especially the { and } braces. |
| Browser shows 502 Bad Gateway | Caddy is fine but Kindie isn't running. Linux: sudo systemctl status kindie. Windows: sc.exe query kindie. See "Running the Application". |
| Signing in sends you back to the sign-in page | BASE_URL in .env doesn't exactly match the address in the browser (Step 6) — fix it and restart Kindie. |
| Uploads fail with a size error | Raise max_size in the Caddyfile, then apply it (see "Later" above). |
Backup & Restore
Backing up and restoring the MongoDB database.
🧰 Before you start: install the backup tools
The commands below need mongodump and mongorestore (MongoDB Database Tools). These are not included with the MongoDB server install and must be installed separately:
- Followed Manual Install (No Docker) already? You installed these as part of that page — nothing more to do.
- Docker Quick Start? The tools already exist inside the
mongodb container — nothing to install on your host. Use docker exec as shown below instead of running mongodump directly.
- Otherwise, download them from the MongoDB Download Center (Windows: run the
.msi; Linux: sudo apt-get install -y mongodb-database-tools; macOS: brew install mongodb-database-tools).
Check they're installed: mongodump --version
💾 Manual backup / restore
Docker Quick Start
Run these in the Kindie folder, using service names — Compose finds the right container for you. Every line needs sudo (Windows: leave it out) and -f docker-compose.prod.yml:
mkdir -p kindie-backup
sudo docker compose -f docker-compose.prod.yml exec mongodb mongodump --db=kindiedb --gzip --archive=/tmp/database.gz
sudo docker compose -f docker-compose.prod.yml cp mongodb:/tmp/database.gz kindie-backup/database.gz
sudo docker compose -f docker-compose.prod.yml cp kindie:/app/assets/uploads kindie-backup/photos
sudo docker compose -f docker-compose.prod.yml cp kindie:/app/.tmp/chat-attachments kindie-backup/chat-files
📷 See the backup on screen
The screenshots were taken on a Windows computer without sudo and without the -f docker-compose.prod.yml flag. On a server, add both to every command, as written above.

1. Create the backup folder

2. Dump the database: the command

2. Dump the database: the result

3. Copy the database file out

4. Copy the photos

5. Copy the chat files

6. Check the backup folder
That's a complete backup — the database, uploaded photos/files, and chat attachments (each stored in its own Docker volume, so a plain file copy of the Kindie folder would miss them). Already have a kindie-backup folder from before? Rename it first, e.g. kindie-backup-old. Download kindie-backup to your own computer with WinSCP/FileZilla afterward — a backup that only lives on the server is lost together with the server.
Restore (⚠️ replaces the current data) — same sudo and -f as above:
sudo docker compose -f docker-compose.prod.yml cp kindie-backup/database.gz mongodb:/tmp/database.gz
sudo docker compose -f docker-compose.prod.yml exec mongodb mongorestore --drop --gzip --archive=/tmp/database.gz
sudo docker compose -f docker-compose.prod.yml cp kindie-backup/photos/. kindie:/app/assets/uploads
sudo docker compose -f docker-compose.prod.yml cp kindie-backup/chat-files/. kindie:/app/.tmp/chat-attachments
sudo docker compose -f docker-compose.prod.yml exec -u root kindie chown -R kindie:kindie /app/assets/uploads /app/.tmp/chat-attachments
📷 See the restore on screen
The screenshots were taken on a Windows computer without sudo and without the -f docker-compose.prod.yml flag. On a server, add both to every command, as written above.

1. Start from empty (only for this demonstration)

2. Start Kindie again: the command

2. Start Kindie again: the result

3. Copy the database file in

4. Restore the database: the command

4. Restore the database: the result

5. Restore the photos

6. Restore the chat files

7. Fix file permissions

8. Sign in: your data is back
Then refresh the browser. Your security keys (.env) live inside a Docker volume, not a plain file on the server — they are not part of this backup and don't need to be; a fresh install on a new server generates its own.
Manual Install (No Docker)
| Action | Command |
| Back up the whole database | mongodump --host 127.0.0.1 --port 27017 --db kindiedb --out ./backup/kindiedb |
| Restore | mongorestore --drop --host 127.0.0.1 --port 27017 --db kindiedb ./backup/kindiedb |
Replace kindiedb with the value of MONGO_DB_NAME in .env. Replace host, port, username, and password when MongoDB is on another server or requires authentication. A complete backup also includes .env and, for disk storage, the assets/uploads and .tmp/chat-attachments directories when they exist.
Restore replaces database records. Take a fresh backup before using --drop, stop the application during restore, and verify the database name and server first.
🤖 Automatic daily backup (recommended)
A script that backs up the database, .env, uploads and chat attachments together in one run, then deletes backups older than 14 days.
Docker Quick Start
Run on the host (not inside a container) — paste this whole box in, in the Kindie folder, to create the script. It needs Docker access, so run it as root:
cat > kindie-backup-docker.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
DEST="kindie-backup/$(date +%Y-%m-%d_%H-%M)"
mkdir -p "$DEST"
docker compose -f docker-compose.prod.yml exec -T mongodb mongodump --db=kindiedb --gzip --archive=/tmp/database.gz
docker compose -f docker-compose.prod.yml cp mongodb:/tmp/database.gz "$DEST/database.gz"
docker compose -f docker-compose.prod.yml cp kindie:/app/assets/uploads "$DEST/photos"
docker compose -f docker-compose.prod.yml cp kindie:/app/.tmp/chat-attachments "$DEST/chat-files"
find kindie-backup -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
echo "[$(date)] Backup saved to $DEST"
EOF
chmod +x kindie-backup-docker.sh
Try it once (sudo ./kindie-backup-docker.sh), then schedule it nightly at 2:30am with sudo crontab -e:
30 2 * * * cd /path/to/kindie && ./kindie-backup-docker.sh >> /var/log/kindie-backup.log 2>&1
Linux (Manual Install)
Paste this whole box in — it writes the script in one go (change MONGO_DB_NAME first if you used something other than kindiedb in the setup wizard):
sudo tee /usr/local/bin/kindie-backup > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
KINDIE_DIR="/opt/kindie"
BACKUP_ROOT="/var/backups/kindie"
MONGO_DB_NAME="kindiedb"
KEEP_DAYS=14
DEST="$BACKUP_ROOT/$(date +%Y-%m-%d_%H-%M)"
mkdir -p "$DEST"
mongodump --host 127.0.0.1 --port 27017 --db "$MONGO_DB_NAME" --gzip --out "$DEST/db"
cp "$KINDIE_DIR/.env" "$DEST/env"
[ -d "$KINDIE_DIR/assets/uploads" ] && tar -czf "$DEST/uploads.tar.gz" -C "$KINDIE_DIR/assets" uploads
[ -d "$KINDIE_DIR/.tmp/chat-attachments" ] && tar -czf "$DEST/chat-attachments.tar.gz" -C "$KINDIE_DIR/.tmp" chat-attachments
chmod -R go-rwx "$DEST"
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime "+$KEEP_DAYS" -exec rm -rf {} +
echo "[$(date)] Backup saved to $DEST"
EOF
sudo chmod 700 /usr/local/bin/kindie-backup
Try it once by hand, then schedule it for every night at 2:30am:
sudo kindie-backup
echo '30 2 * * * root /usr/local/bin/kindie-backup >> /var/log/kindie-backup.log 2>&1' | sudo tee /etc/cron.d/kindie-backup
Check it worked the next morning: sudo tail /var/log/kindie-backup.log. Prefer a downloadable file instead? 📄 Download backup-kindie.sh.example.
A backup on the same server is not enough. If the server is lost, so are its backups. At least once a week, copy the latest backup folder somewhere else, e.g. to your own computer: scp -r root@203.0.113.10:/var/backups/kindie/2026-10-01_02-30 .
Windows (Manual Install)
📄 Download backup-kindie.ps1.example — edit the settings at the top, then schedule it with Task Scheduler (instructions inside the file).
📦 Moving to a new computer or server
Same school and data, different hardware — not the same as "Updating Kindie" (same machine, newer version) or "Reset & Reinstall" (wipes data). The key rule either way: do not finish the setup wizard on the new machine — finishing it creates a brand-new, empty school that conflicts with the one you're about to restore.
Docker Quick Start
- Install Kindie on the new machine ("Docker Quick Start") and start it, but when the setup wizard appears, leave it open without clicking Finish.
- Put the
kindie-backup folder from the old install into the new Kindie folder, then restore following "Manual backup / restore" above.
- Copy
kindie.env and the .secrets folder from the old install into the new Kindie folder too — these hold your address, mail, and Firebase settings. Apply them: sudo docker compose -f docker-compose.prod.yml up -d.
- Refresh the browser — since the database now already has a school, Kindie skips straight to the sign-in page. Sign in with an existing account.
Manual Install (No Docker)
- Extract Kindie on the new machine ("Manual Install (No Docker)") but do not start it yet.
- Copy the old install's
.env and .install.lock into the new folder before the first start — their presence is what tells Kindie to skip the wizard entirely.
- Restore the database backup (see "Manual backup / restore" above), and copy back
assets/uploads / .tmp/chat-attachments if you have them.
npm install, then start Kindie — it opens straight to the sign-in page.
🔄 Updating Kindie to a new release
Always back up first (above) — an update should never be your first backup. The rest depends on how you installed.
Manual Install (No Docker)
Step 1: Stop the application
- systemd:
sudo systemctl stop kindie
- Windows/WinSW:
Stop-Service kindie
Step 2: Replace the application files
- Extract the new release into a new folder next to the old one (don't overwrite it in place — this is what makes rolling back possible). Copy your existing
.env and .install.lock into the new folder — a new release must not run the setup wizard again over an existing school. MongoDB and Redis are separate services, unaffected by which folder the app code lives in.
Step 3: Install and start
npm install, then point your service supervisor (systemd unit file / WinSW's <workingdirectory>) at the new folder and start it — see "Running the Application".
Docker Quick Start
Do not extract the new release into a differently-named folder — Compose names its data volumes (mongo-data, uploads, config) after the folder, so switching folders can make it look like your data disappeared (it's still there, just under the old folder's volume names).
Step 1
- In the same folder you already have, extract the new release on top of the old files, except: keep your existing
kindie.env and .secrets folder — don't let the new release's files overwrite them. They hold your address, mail, and Firebase settings, so there's nothing to re-edit.
Step 2
sudo docker compose -f docker-compose.prod.yml up -d --build --force-recreate
Check it worked
Open the site, sign in, and look at the logs (see "Running the Application") for startup errors before considering the update done.
⏪ Rolling back
Manual Install (No Docker): stop the new version, point your service supervisor back at the old folder (the one you kept, untouched), and start it again.
Docker: if you kept the old release files elsewhere (a zip or a copy made before Step 1 above), restore them into the same folder and run sudo docker compose -f docker-compose.prod.yml up -d --build again — the volumes are untouched either way, since they don't depend on the release files.
Either way: if the new version already changed data in a way the old version doesn't expect, restore the database backup taken before the update too — this is exactly why backing up first is not optional.
🗑️ Reset & Reinstall
Start again from an empty installation — for example after a trial run with sample data. This permanently deletes all school data: students, parents, invoices, photos.
Take a backup first if there's anything you might want back (see above).
Manual Install (No Docker)
sudo systemctl stop kindie
mongosh --quiet kindiedb --eval "db.dropDatabase()"
sudo rm -f /opt/kindie/.env /opt/kindie/.install.lock
sudo rm -rf /opt/kindie/assets/uploads /opt/kindie/.tmp/chat-attachments
sudo systemctl start kindie
Docker
sudo docker compose -f docker-compose.prod.yml down -v && sudo docker compose -f docker-compose.prod.yml up -d --build
-v also deletes the mongo-data, uploads and config volumes — this is the part that actually wipes the data and security keys.
Uninstall Kindie (Docker)
This deletes Kindie and all your school's data. Take a backup first (see above) if you may need the data again.
sudo docker compose -f docker-compose.prod.yml down -v --rmi all
--rmi all also removes the mongo:8.0 image, which other programs on the same computer may share — it is downloaded again the next time something needs it. Then delete the Kindie folder, after moving out any kindie-backup folder you still need. Remove Docker itself only if nothing else uses it.
Either way, replace kindiedb if your database has another name. Kindie starts in setup mode again — open it and go through "First Run" once more. Caddy and HTTPS stay as they are.
Web Customization
Make Kindie your own: use a different web server, change the Web Admin colors and language, swap the logo and icons, and change the splash screen. Everything here is done by editing files — no change to Kindie's code logic is needed.
Two things can be customized, and they work a little differently. The
Web Admin is already built into
assets/react-backend-dist/; to change its wording you edit the source in
react-backend/ and rebuild it (needs Node.js 20.11). The
Kindie App has its own page,
App Customization. Logos and icons of the Web Admin can also be swapped directly in the built folder, with no rebuild.
🌐 Change the default web server
Kindie's own server listens on port 1337 and is meant to sit behind a web server that handles your domain name and HTTPS. The default is Caddy (bundled with the Docker production setup; see "Production Deployment" for Manual Install). You can use Nginx, Apache or IIS instead — whichever you already run.
For a normal installation keep Caddy: it already handles HTTPS and WebSocket, and with Docker you only edit kindie.env (BASE_URL) and leave docker-compose.prod.yml and Caddyfile as they are. Replacing it is an advanced task for a system administrator: the other web server must reach Kindie, keep WebSocket working, and never expose MongoDB or Redis to the Internet.
What any web server must do
| Requirement | Why |
Forward every request to http://127.0.0.1:1337 | That is where Kindie listens. Keep port 1337 itself closed to the Internet. |
| Serve your domain over HTTPS with a valid certificate | Sign-in cookies are marked secure, and browsers only allow push notifications and installing the app over HTTPS. |
Pass the Host header and send X-Forwarded-Proto: https | Kindie uses them to know it is served over HTTPS. Without them, signing in sends you straight back to the sign-in page. |
Let WebSocket through on /socket.io | Live chat. Without it, messages only appear after a refresh. |
| Accept uploads of at least 100 MB | Chat videos and large files. Many servers default to 1 MB. |
BASE_URL set to your public address | In .env (Manual Install) or kindie.env (Docker): https://your-school.com, no / at the end. |
🐧 Nginx (Linux)
- Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
- Download 📄 nginx-kindie.conf.example, replace
your-school.com with your domain, and save it as /etc/nginx/sites-available/kindie.conf. Its core is:
server {
listen 80;
server_name your-school.com;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:1337;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
}
}
- Enable it, test it, and get the free HTTPS certificate (Certbot also edits the file for you):
sudo ln -s /etc/nginx/sites-available/kindie.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d your-school.com
✅ https://your-school.com opens Kindie with a padlock.
🐧 Apache (Linux)
- Install Apache and Certbot, switch on the needed modules, and get a certificate (this also creates the HTTPS site):
sudo apt install -y apache2 certbot python3-certbot-apache
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
sudo certbot --apache -d your-school.com
- Open the HTTPS site Certbot created (
/etc/apache2/sites-available/…-le-ssl.conf) and add these lines inside <VirtualHost *:443>:
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
LimitRequestBody 104857600
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/(.*) ws://127.0.0.1:1337/$1 [P,L]
ProxyPass / http://127.0.0.1:1337/
ProxyPassReverse / http://127.0.0.1:1337/
- Check and reload:
sudo apachectl configtest && sudo systemctl reload apache2
🖥️ IIS (Windows Server)
- Install the Application Request Routing and URL Rewrite modules, and the Windows feature WebSocket Protocol (Server Manager → Add roles and features → Web Server → Application Development).
- In IIS Manager click the server name → Application Request Routing Cache → Server Proxy Settings… → tick Enable proxy → Apply.
- Still on the server level, open URL Rewrite → View Server Variables… → Add… →
HTTP_X_FORWARDED_PROTO.
- Create an IIS site bound to your domain (ports 80 and 443) with its physical path set to an empty folder, and copy 📄 web.config.example into that folder, renamed to
web.config.
- Get a free certificate with win-acme: run
wacs.exe as Administrator and follow the prompts — it also binds the certificate to your site.
✅ https://your-school.com opens Kindie with a padlock.
Switching off Caddy
Caddy and your new web server cannot both use ports 80 and 443, so stop Caddy first.
- Manual Install: Linux —
sudo systemctl disable --now caddy; Windows (PowerShell as Administrator) — Stop-Service caddy and Set-Service caddy -StartupType Disabled.
- Docker (production): open
docker-compose.prod.yml. In the kindie service, replace expose: - "1337" with ports: - "127.0.0.1:1337:1337", so your web server on the same machine can reach it. Delete the whole caddy: service block. Then apply: sudo docker compose -f docker-compose.prod.yml up -d. Keep BASE_URL in kindie.env set to your public https:// address.
🌍 Customize the language
Kindie has three places where language lives. Change only the ones you need.
1. The school's default language
In the Web Admin go to Settings → General and set School default language (Tiếng Việt, English, Français, Português, Русский or ไทย). It controls push notifications, invoice labels, outgoing emails, and the starting language of the app for users who have not chosen one. It is set to English during the first-run wizard.
2. The Web Admin screens
- Each person picks their own language from the language menu in the Web Admin's header (English, Español, Français, Português, Русский, Tiếng Việt). The choice is remembered in that browser.
- Change the starting language (what a first-time visitor sees): open
react-backend/src/i18n/i18n.ts, find const DEFAULT_LANGUAGE = … and set it to the language you want, for example LanguageCode.FR. The list of codes is react-backend/src/models/language.ts.
- Change a word or sentence: the texts are in
react-backend/src/i18n/locales/<language>/ (shared words) and in each page's locales/<language>.ts file, for example react-backend/src/pages/Albums/locales/en.ts. Search for the old text, edit it, and do the same in the other languages you use.
- Check you did not break anything:
npm run i18n in the react-backend folder lists any text that exists in one language but is missing in another.
- Apply: rebuild the Web Admin — see "Rebuild and apply your changes" at the end of this page.
Adding a brand-new language to the Web Admin is a bigger job: copy every en file to the new language code, add the code in models/language.ts, in src/i18n/i18n.ts (the loader list and a dayjs/locale import), in the header's flag list and in scripts/check-i18n.mjs, then run npm run i18n until it passes.
3. Emails, notifications and sample data
- Texts sent by the server are in
config/locales/<language>.json (en, vi, fr, pt, ru, es, th, tr, sr). Edit the wording there.
- Email layouts are in
api/emailTemplates/<name>/html.ejs.
- Restart Kindie after editing these files — Linux
sudo systemctl restart kindie, Windows Restart-Service kindie, Docker sudo docker compose -f docker-compose.prod.yml up -d --build.
🎨 Change the Web Admin colors
This changes the main brand color of the Web Admin (buttons, links, selected menu items). It does not recolor every status color or every component.
🖼️ Change the logo and icons
Your school's logo
Upload it once in the first-run wizard, or change it later in the Web Admin on the Profile page, in the school section. Kindie uses it in emails (password reset, account activation) and as the school's branding.
Web Admin logo, favicon and page title
| What | File to replace |
| Logo in the side menu | images/logo_vertical.png |
| Small logo (collapsed menu) | images/logo_no_text.png |
| Browser-tab icon and the loading screen logo | images/logo.png |
| Classic favicon | favicon.ico |
| Browser-tab title | The <title> line in index.html (default "Kindie - Kindergarten Management") |
- Put your files in
react-backend/public/ (public/images/…, public/favicon.ico) with the same file names, and edit the title in react-backend/index.html. If you rename a file, update every matching reference in react-backend/index.html and react-backend/src/constants/page.ts. Then rebuild — see "Rebuild and apply your changes" at the end of this page.
- Keep the favicon square and keep transparent logos in PNG format. The files under
public/icons/ also include functional interface icons — replace those only when you mean to change the matching icon.
- Browsers keep these images for a long time: press Ctrl + F5 to see the new ones. On Docker, run
sudo docker compose -f docker-compose.prod.yml up -d --build so the new files go into the image.
The title in the browser tab is fixed text. The Website Name you typed in the setup wizard does not change it.
Logo in emails
Emails show your school's logo. When a school has none, they use assets/images/logo-icon.png and logo-icon.svg — replace those two files to change that fallback.
💦 Change the splash screen
Web Admin loading screen
While the Web Admin loads it shows images/logo.png pulsing on a white background — the same file as the browser-tab icon, so replacing it (see above) changes both. The look of the loader itself is the <style> block at the top of react-backend/index.html.
🔁 Rebuild and apply your changes
| You changed… | Do this |
Web Admin wording, colors, default language, logos or icons, index.html, or any file in react-backend/ | Needs Node.js 20.11. In the Kindie folder run npm ci --prefix react-backend (first time only), then npm run build:admin. The result is written to assets/web-admin-dist/, the folder Kindie serves. Then restart Kindie, or on Docker run sudo docker compose -f docker-compose.prod.yml up -d --build, and press Ctrl + F5 in the browser. |
Server texts (config/locales) or email templates | Restart Kindie (Docker: sudo docker compose -f docker-compose.prod.yml up -d --build). |
| Kindie App (colours, language, logo, icons, start screen) | Depends on what you changed: see App Customization → Rebuild and apply your changes. |
| Web server (Nginx, Apache, IIS) | Reload the web server and open your address to check the padlock and that a chat message appears without refreshing. |
On Windows, leave out sudo in Docker commands.
Keep a copy of the files you changed (or use Git). Installing a new Kindie version replaces the package's files, and you will need to apply your changes to the new ones again.
App Customization
Make the Kindie App look like your school's: server address, colours, starting language, logo, icons and start-up screen. For the web app (PWA) these are edits to a few files, with no rebuild.
Two ways to apply a change: Web app (PWA): no rebuild. The ready-made app in dist/ reads dist/config.js and the pictures next to it, so you edit a file, save it and reload the page. Android and iPhone apps are built from the source code, so what you change is fixed into the app when you build it. Each section below says which case applies.
Before you start: If you rebuild the web app (yarn build), the build replaces dist/ with a fresh copy made from public/. Make the same edits in public/config.js and public/manifest.json (and put your pictures in public/) first, or they are lost. The ready-made dist/ you received does not need a rebuild. If you run the app with Docker on your computer, run docker compose restart after each change.
| I want to change… | Edit | Section |
| The server the app talks to | dist/config.js (apiUrl, fileUrl) | Server address |
| The colours | dist/config.js (theme), dist/manifest.json | Colours |
| The language the app starts in | dist/config.js (language) | Language |
| The logo and the icons | dist/config.js (logoUrl), the pictures in dist/icons/ and dist/favicon/ | Logo and icons |
| The start-up screen | dist/config.js (theme), dist/manifest.json; phone apps: app.config.ts | Start-up screen |
🌐 Server address
The app ships pointing at http://localhost:1337. Put the address of your Kindie server in dist/config.js:
apiUrl: "https://kindie.myschool.com",
fileUrl: "https://kindie.myschool.com",
No / at the end. The full explanation, including the phone apps, is in Mobile App Setup → Settings (dist/config.js).
🎨 Colours
Two brand colours, in dist/config.js:
theme: {
primary: "#c2410c", // main colour: buttons, active tabs, links, icons
secondary: "#fde7d6", // soft background colour
},
- Write each colour as
# followed by six digits or letters (#c2410c). Anything else is ignored and the built-in colour stays.
- Leave a value empty (
"") to keep the built-in colour (#3c7f8b and #d0e7eb).
- You choose only the two colours. The darker, lighter and pale shades of the main colour (pressed buttons, tinted backgrounds) are worked out by the app.
- Pick a main colour that white text is readable on. Buttons have white text, so a pale colour such as yellow makes it hard to read. The built-in teal has a contrast of 4.6 : 1; stay above 4.5 : 1 with any contrast checker (search for "WCAG contrast checker").
- Only the brand colours change: the buttons, active tabs, links and icons in your colour, and the soft background behind them. Greys, field borders and the colours that carry a meaning (green for success, red for errors, amber for warnings) stay as they are, on purpose.
The browser bar and the installed app
The colour of the browser's address bar, and of the page before the app has loaded, follows theme.secondary automatically. The colours Android uses for an installed web app live in a separate file, dist/manifest.json. Change the two lines there to the same values:
"background_color": "#fde7d6",
"theme_color": "#fde7d6",
Someone who already installed the app keeps the old colours until they remove it from their home screen and install it again.
Android and iPhone apps
They have no config.js. Open src/theme/tokens.ts and change DEFAULT_PRIMARY and DEFAULT_SECONDARY (near the top), and every #d0e7eb in app.config.ts (four colour settings: splash, Android icon background, web backgroundColor and themeColor). Then build the app again.
🌍 Language
The app speaks English, Vietnamese, French, Portuguese, Russian, Chinese and Thai. In dist/config.js:
language: "vi",
Use one of en, vi, fr, pt, ru, zh, th. Empty, or a code that is not in this list, means English. This is only the starting language. In this order, the app uses:
- the language the person chose in the app, on that device;
- the language your school has set in the Web Admin (Settings → General → School default language);
- the
language value above (English when empty).
So a phone or browser that has already been used keeps its own choice. To see your change, open the app in a private / incognito window.
Note: The Android and iPhone apps take the language from step 2 (the school's setting in the Web Admin). To change the fallback of step 3 in the phone apps, open src/config/config.base.ts, find DEFAULT_LNG, replace the last EN_LNG with VI_LNG (or another *_LNG defined just above it), and build the app again.
Change a word, or add a language
- Texts are in
src/i18n/translations/: en.ts, vi.ts, fr.ts, pt.ts, ru.ts, th.ts, zh.ts. Edit the text on the right of each key; never rename the keys.
fallbackLng in src/i18n/index.ts is the language used when a text is missing.
- To add a language: copy
vi.ts to a new file (for example es.ts), translate it, import it and add it under resources in src/i18n/index.ts, and add the code to AvailableLng and the language list in src/constants/Language.ts.
- Check:
yarn check-i18n — every language needs the same keys as vi.ts, with no empty values. Then yarn build and upload dist/ again.
🖼️ Logo and icons
There are three different pictures. Replace the ones you need.
The logo inside the app
It appears on the start screen, in the "Install Kindie" box and in the notification permission box. Put your logo file in the dist/ folder, for example dist/my-logo.png, and name it in dist/config.js:
logoUrl: "/my-logo.png",
A full address also works ("https://myschool.com/logo.png"). Use a square PNG, at least 192 × 192 pixels. If the logo does not show, open the same address in the browser (for example https://app.myschool.com/my-logo.png): it must show your picture. Empty means the built-in logo (assets/logo.png, which you can also replace and rebuild).
The icon of the browser tab and of the home-screen app
Replace these three files with your own pictures. Keep the file names and the sizes:
| File | Size | Used for |
dist/favicon/favicon.png | 192 × 192 | Browser tab, iPhone home screen |
dist/icons/192.png | 192 × 192 | Android home-screen icon |
dist/icons/512.png | 512 × 512 | Android home-screen icon, start-up screen |
Browsers remember icons for a while: reload with Ctrl + F5 (Cmd + Shift + R on a Mac). An app that was already installed keeps its old icon until it is removed and installed again. The app's name (name, short_name) is in public/manifest.json too.
The icon of the Android and iPhone apps
These are separate files used when the phone app is built. Replace the pictures, keep the file names. The ones shipped with the package are small samples: a 192-pixel icon looks blurry on a phone and is rejected by the stores.
| File | What it is | Size |
assets/kindie_icon.png | The app icon (iPhone, store listings, web) | 1024 × 1024 pixels, PNG, no transparency |
assets/ic_launcher_adaptive_fore.png | Android launcher icon: your logo only, on a transparent background | 1024 × 1024 pixels, PNG, transparent background, logo kept inside the central two thirds |
assets/ic_launcher_adaptive_back.png | The picture shown on the start-up screen | At least 1024 × 1024 pixels, PNG |
💦 Start-up screen
Web app (PWA)
- Before the app has loaded: the page background is the
theme.secondary colour from the Colours section.
- Start screen: when the app opens it shows your logo (see "The logo inside the app") at 190 × 190 on a plain light background, with a loading spinner.
- Launch screen of the installed app (Android): the phone builds it from
public/manifest.json — the 512 × 512 icon, the name and the background_color. Change those three.
- iPhone: iOS shows a plain screen in the
background_color; it does not use a custom launch picture for web apps.
Android and iPhone apps
In app.config.ts:
splash: {
image: "./assets/ic_launcher_adaptive_back.png", // the picture shown while the app starts
resizeMode: "contain",
backgroundColor: "#d0e7eb", // the colour behind the picture
},
Replace the picture assets/ic_launcher_adaptive_back.png (keep the name; at least 1024 × 1024 pixels, PNG) and change backgroundColor to your colour. The splash screen is part of the built app: delete the android/ and ios/ folders and build again, then uninstall the old app from the phone.
🔁 Rebuild and apply your changes
| You changed… | Do this |
dist/config.js (server, colours, language, logo) or a picture in dist/ | No rebuild. Save and reload the page (Ctrl + F5). Docker on your computer: docker compose restart. |
dist/manifest.json or the icons | No rebuild. An app that is already installed needs to be removed and installed again. |
Texts, src/theme/tokens.ts, src/config/config.base.ts, assets/logo.png | In the app folder run yarn build, then upload the whole dist/ folder again. Make the same edits in public/ first if you also changed config.js or manifest.json. |
Android or iPhone app (anything in app.config.ts, the icons, the splash screen) | Delete the android/ and ios/ folders, uninstall the app from the phone, then build again. |
Check your changes: Reload the app in a private window (no old settings are remembered). On the sign-in screen you should see your colours and your language. If a change does not show, check the quotes and commas in config.js (every line ends with a comma), and that you edited the file in the folder that is actually being served. A mistake in config.js never stops the app from opening, but it falls back to the built-in values, so check that sign-in still works.
Important: Keep a copy of the files you changed (or use Git). Installing a new Kindie version replaces the package's files, and you will need to apply your changes to the new ones again.
Mobile App Setup
Install and run Kindie App for parents, teachers and drivers. Start with the web app (PWA) on your computer, then put it online on your server. The Android and iPhone apps have their own pages. Version 5.0.0, step by step, with screenshots.
Note: The Kindie app connects to your Kindie backend server (the Web Admin package installed in the previous sections). This package contains the full source code and the ready-made web app in the dist/ folder. The native android/ and ios/ projects are not included: they are created automatically the first time you run the Android or iPhone steps.
🧭 Where do I start?
📖 How to read these pages
- Pick one path. The web app needs no phone tools and is the quickest way to see Kindie working. The Android and iPhone apps are built on your own computer from this same package.
- Commands are shown in dark boxes. Type them in a terminal opened inside the project folder (the folder that contains
package.json). Use the Copy button to avoid typing mistakes.
- macOS and Linux share one set of steps. Where a command differs, the 🐧 Linux line is shown next to the 🍎 macOS one. Screenshots were taken on macOS from the release package. 🪟 Windows steps are written out in the text; the results look the same.
- Screenshots do not show the Firebase console, Android Studio, Xcode or Docker Desktop windows, nor a real phone. The text explains those steps.
💻 How to open a terminal in the project folder
A terminal is a window where you type commands. First unzip the package (right-click the .zip file and extract it), then open the terminal inside the new folder.
- 🪟 Windows 11: open the folder in File Explorer, right-click an empty space → Open in Terminal. Windows 10: hold Shift, right-click an empty space → Open PowerShell window here.
- 🍎 macOS: open Terminal (Cmd+Space, type "Terminal"), type
cd (with a space), drag the project folder into the window, press Enter.

Terminal after typing cd kindie-app-v5.0.0 and ls: the project files are listed.
Important: For the Android build, unzip to a short path (for example C:\kindie on Windows): long folder names make the Android build fail.
🖥️ Run the app on your computer
Three ways to open the web app on your own computer. Pick one:
What to download first (once):
| Option | Download |
| Option 1 | Node.js, any current LTS version — nodejs.org/en/download (Windows: .msi; macOS: .pkg; Linux: your package manager or nvm) |
| Option 2 | Docker Desktop (Windows, macOS) — docker.com; on Linux, Docker Engine (see the box in Option 2) |
| Option 3 | Node.js 20.11.x exactly, and Yarn (npm install -g yarn) — see Option 3 |
▶️ Option 1 · Without Docker (double-click or one command)
No Docker, one program to install: Node.js (any current LTS version). 🪟 Windows: double-click start.bat (steps below). 🍎 macOS / 🐧 Linux: run one command (see the end of this option).
- Unzip the package first: right-click the downloaded
.zip → Extract All… → open the new folder it creates. (Running start.bat from inside the zip viewer does not work.)
- Install Node.js once — see Install Node.js on Windows below.
- Double-click
start.bat in the new folder. A black window opens; keep it open. The first start downloads a small helper, so it needs internet; if Windows asks about the firewall, click Allow.
- Open
http://localhost:8080 in your browser. You will see the Kindie sign-in screen.
Important: Signing in will not work yet. The app does not know where
your school's Kindie server is. Open
dist\config.js with Notepad, put your server's address in
apiUrl and
fileUrl, save, and reload the page — see
Settings (dist/config.js). Without a server, you can still see that the app runs.
To stop the app, close the black window (or press Ctrl + C).
Install Node.js on Windows
- Go to nodejs.org/en/download and download the Windows Installer (.msi). For
start.bat, the current LTS version is fine.
- Double-click the file and keep clicking Next until it finishes. No special settings are needed.
- Close and re-open any window you had open, then double-click
start.bat again.
Note: Only if you later
rebuild the app or make the
phone apps do you need
exactly Node.js
20.11.0. Download it from
nodejs.org/en/download/archive/v20.11.0 (file
node-v20.11.0-x64.msi; macOS: the
.pkg).
🍎 macOS / 🐧 Linux (no start.bat)
- Install Node.js, any current LTS version, from nodejs.org/en/download (macOS: the
.pkg; Linux: your package manager or nvm). Open a new terminal afterwards.
- Open a terminal in the unzipped project folder (see Open a terminal above) and run:
npx serve -s dist -l 8080

npx serve shows Serving! with the local address http://localhost:8080.
- Open
http://localhost:8080 in your browser. You will see the Kindie sign-in screen. To stop the app, press Ctrl + C.
Next: put it online for parents → Put the app online. Update later → Update to a new version.
🐋 Option 2 · With Docker
First start Docker Desktop (see the box below) and wait until it says it is running. Then type these commands in a terminal opened inside the project folder (see how to open a terminal).
Docker installation guides (Windows / macOS / Linux)
- 🍎 macOS Download Docker Desktop from docker.com, drag it to Applications, and launch it. Wait until the whale icon in the menu bar stops moving and Docker Desktop says it is running (about a minute). If a command says it cannot connect to Docker, it is not running yet.
- 🪟 Windows Download Docker Desktop from the same page, run the installer and keep "Use WSL 2" ticked. It asks for administrator rights and a restart. After the restart open Docker Desktop from the Start menu and wait until the whale icon says it is running ("Engine running"). If a command says it cannot connect to Docker, Docker Desktop is not running yet.
- 🐧 Linux
curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker $USER. Then log out and log in again so that the docker group applies (or put sudo in front of every docker command).
- Start the app. The first start builds it inside Docker (several minutes, needs internet). Later starts take a few seconds.
docker compose up -d

The Docker build finishes with Image Built, Network Created and Container Started.
- Check that it is running. The container must show Up and port 8080.
docker compose ps

docker compose ps shows the kindie-app container Up on port 8080.
- Open
http://localhost:8080 in your browser.

The Kindie sign-in screen at http://localhost:8080 with the Parent, Teacher and Driver choices.
Note: On the very first start, Docker may print a yellow line like "pull access denied for kindie-app …" before it builds. It is harmless: Docker first looks for a ready-made image and then builds it from this package.
After you change the source code or install a new version, rebuild with:
docker compose up -d --build
To stop the app, run:
docker compose down

docker compose down removes the container and the network.
🎬 Video: Run the web app with Docker (macOS)
The whole installation from start to finish, without sound. English captions are on the screen (about 1 min).
🧩 Option 3 · From source (developers)
First check your Node.js version: node -v must print v20.11.x. If it does not:
- 🪟 Windows Install it as described in Install Node.js on Windows above (the
node-v20.11.0-x64.msi file).
- 🍎 macOS With nvm run
nvm install 20.11.0 && nvm use 20.11.0 (add nvm alias default 20.11.0 to keep it in new windows). Without nvm, install node-v20.11.0.pkg from nodejs.org/en/download/archive/v20.11.0, then open a new Terminal.
- 🐧 Linux Install nvm first, open a new terminal, then run
nvm install 20.11.0 && nvm use 20.11.0 and nvm alias default 20.11.0.
Install Yarn once with npm install -g yarn. Then check both versions, in a terminal opened inside the project folder:
node -v
yarn -v

node -v prints v20.11.0 and yarn -v prints 1.22.22.
Install the libraries:
yarn install

yarn install in progress: five steps and a progress bar.
It takes a minute or two. It is finished when the last line says Done. The yellow warning lines are normal.

yarn install finished: the last lines say Applying patches and Done in 13.59s.
Then pick one of the two ways to run it:
3a. Development mode with hot reload
yarn web

yarn web just after it starts.

yarn web is ready: Web is waiting on http://localhost:8081.
Open http://localhost:8081 in your browser. The first time, the page stays blank for a minute or two while the app is prepared; the terminal then prints Web Bundled. Later reloads are fast.

After the first page load the terminal prints Web Bundled.

The sign-in screen served by yarn web at http://localhost:8081.
Tip: yarn web keeps running in its terminal window. Do not type other commands in that window (it reacts to single key presses). To stop it, press Ctrl + C. Open a second terminal if you need to run other commands.
3b. Serve the ready-made dist/ folder (the same command as Option 1)
npx serve -s dist -l 8080

npx serve shows Serving! with the local address http://localhost:8080.
No build is needed. Open http://localhost:8080.
What you will see — all of it is normal
yarn install prints many lines that start with warning (about "peer dependency" or "Resolution field ... is incompatible"). They are harmless: the install worked if the last line says Done.
yarn web prints [kindie] KINDIE_IOS_BUNDLE_ID / KINDIE_ANDROID_PACKAGE are not set. That only matters for the phone apps. The Docker build of Option 2 prints it too.
- The first time you open
http://localhost:8081, the page stays blank for a minute or two while the app is prepared. Later reloads are fast.
yarn web can use another server address than config.js — see "Which file do I edit?" in Settings (dist/config.js).
3c. Build the app for production (after you changed the code)
- Put your server address in
public/config.js first (see Settings below). A build replaces dist/config.js with a copy of public/config.js.
- Run the build:
yarn build
- It writes the ready-to-deploy files to
dist/. The last step downloads a small tool (workbox-cli), so the computer needs an Internet connection.
- To put the new
dist/ on your server, follow Install a new version in Production with Docker or Production without Docker.
🎬 Video: Run the web app from source with Yarn (macOS)
The whole installation from start to finish, without sound. English captions are on the screen (about 1 min).
⚙️ Settings (dist/config.js)
The web app reads its settings from dist/config.js (the ready-made app, start.bat, Docker and your server all use this file). Open it with any text editor (Notepad on Windows, TextEdit on macOS).

dist/config.js opened in a text editor: apiUrl and fileUrl point to http://localhost:1337.
apiUrl: "http://localhost:1337",
fileUrl: "http://localhost:1337",
To point to a live production server, replace the two addresses and save the file:
apiUrl: "https://kindie.myschool.com",
fileUrl: "https://kindie.myschool.com",

dist/config.js after replacing both addresses with https://kindie.myschool.com.
| Setting | Description |
apiUrl | Address of your Kindie server (no trailing /). |
fileUrl | Address where photos and files are served (same as apiUrl). |
firebase, vapidKey | Firebase Web Push settings (see Push notifications). |
Tip: Using Docker on your computer? (On a live server you do not need this.) After you save dist/config.js, run docker compose restart in the project folder. The container reads the file only when it starts; no rebuild is needed.
docker compose restart

docker compose restart restarts the container so it reads the new config.js.
Which file do I edit?
- Using the ready-made app (
start.bat, Docker, your server): edit dist/config.js.
- Rebuilding the app from source (
yarn build): edit public/config.js first. A rebuild replaces dist/config.js with a copy of public/config.js, so changes made only in dist/ are lost.
- Running from source with
yarn web: if a .env.local file exists, its EXPO_PUBLIC_API_URL / EXPO_PUBLIC_FILE_URL win over config.js. Without those two lines, public/config.js is used. So if you made a .env.local for the phone apps, yarn web talks to the server named there.
- Android / iPhone apps do not read
config.js. They take the server address from EXPO_PUBLIC_API_URL and EXPO_PUBLIC_FILE_URL (copy .env.example to .env.local for testing on your own phone). These addresses are fixed into the app when it is built. A phone app you give to parents must use an https:// address — plain http:// addresses are blocked on release builds.
🔄 Update to a new version (on your computer)
- Stop the old copy first: close the old
start.bat window, or run docker compose down in the old folder. (Your current version number is on line 4 of package.json.)
- Unzip the new version into a new folder. Do not unzip over the old one.
- Copy your own files from the old folder into the new one, replacing what is there:
dist/config.js (your server address and push settings), and .env.local, google-services.json, GoogleService-Info.plist (if you use the phone apps).
- Start it the same way as before:
start.bat — double-click it in the new folder. Docker — docker compose up -d --build in the new folder (--build matters: without it Docker keeps running the old copy). From source — yarn install again (the libraries may have changed), then yarn web.
- Phone apps: compare
.env.local with the new .env.example (settings may have been added), delete the android/ and ios/ folders so they are generated again, and read docs/publishing.md, section 5.
- Read
CHANGELOG.md for anything that needs your attention.
On a live server the steps are different: see PRODUCTION-DOCKER.md or PRODUCTION-MANUAL.md, section "Install a new version".
🌐 Put the app online (production)
Two guides, depending on how Kindie itself was installed on your server. Each one starts with a box that tells you whether it is the right one for you.
Which guide is yours?
- When you installed Kindie, you set its address in a file named
kindie.env → Kindie runs with Docker: use Production with Docker.
- You set its address in a file named
.env in /opt/kindie, or the app gets a server of its own → use Production without Docker.
The full steps, with screenshots, are in the three sections below:
🐋 Production with Docker (Kindie runs with Docker)
Applies to Kindie App 5.0.0 and newer, together with Kindie 5.0.0 and newer. This part puts the Kindie App on the Internet at its own address, such as https://app.myschool.com, so that parents, teachers and drivers can open it on their phones. The app runs on the same server as Kindie and shares its HTTPS certificate handling — there is nothing extra to buy or install. It is for a server where Kindie runs with Docker.
You do not need to be technical. Every step tells you where to click or what to paste, and ends with a ✅ check so you know it worked before you go on. Only want to try the app on your own computer? Use Run the app on your computer.
✅ What you need
| Item | Details |
| Kindie, already live | The package "Kindie — Kindergarten Management" running on your server at its own address, for example https://kindie.myschool.com. Install it first with the PRODUCTION-DOCKER.md of that package, and finish its setup wizard. |
| The server's login | The same three things you used to install Kindie: the server's IP address (four numbers such as 203.0.113.10), the user name (often root or ubuntu) and the password. They are on your server provider's website and in the email it sent when the server was created. |
| Your domain's DNS | The login to the website where you bought your domain. |
| The Kindie App package | This folder, unzipped on your computer. |
Two addresses — don't mix them up
| Address | Example | Who uses it |
| Kindie's address | https://kindie.myschool.com | School staff, for the admin site. You already have it. |
| The app's address | https://app.myschool.com | Parents, teachers and drivers. You create it in Step 1. |
Important: The examples are not yours. kindie.myschool.com, app.myschool.com, the user name root and the IP address 203.0.113.10 are made-up values that show what a line looks like. Wherever you see one, type your own instead. Everything else in a command is typed exactly as shown.
Two machines, two places to work. Your computer is the one in front of you. The server is the rented machine: you give it commands in a terminal that is connected to it. Each step says which one you are working on. On the server, the line you type on starts with the server's name (such as root@my-server or ubuntu@my-server), not your computer's.
📚 Words used in this guide
- IP address — the server's number on the Internet, four numbers such as
203.0.113.10.
- DNS — the Internet's phone book. It tells browsers which server a web address (such as
app.myschool.com) belongs to. You set it on the website where you bought your domain.
- Terminal — a window where you type a command and press Enter to run it. A command is finished when a new empty line appears and you can type again. Stuck — nothing happens for minutes, or the line you type on stays at
>? Press Ctrl + C and paste again.
- HTTPS — the padlock in the browser. It keeps what people type private. It needs the DNS step to be finished first.
Note: Screenshots in this part were taken on macOS (Terminal and FileZilla). On Windows, PowerShell and WinSCP work the same way. The addresses in the screenshots are replaced by the examples above (203.0.113.10, kindie.myschool.com, app.myschool.com). The DNS website of your domain is not shown: the text explains it.
📦 Install
Six steps, about 20 minutes. Do them in order.
Step 1 — Point the app's address to the server
On your computer, in the browser.
- Sign in to the website where you bought your domain and open its DNS settings.
- Add one new record, next to the one you made for Kindie. Do not change the others.
| Type | Name | Value |
A | app — only the first word of the app's address | your server's IP address — the same one as for Kindie |
- Save.
Tip: Is your domain managed at Cloudflare? Set Proxy status to DNS only — click the orange cloud so that it turns grey.
✅ Check: open dnschecker.org, type the app's address without https:// (e.g. app.myschool.com), choose type A, and search. You should see your server's IP address. This can take from a few minutes to a few hours. You can do Steps 2 to 4 while you wait, but not Step 5.
Step 2 — Tell the app where Kindie is
On your computer.
- Open the app folder, then the folder
dist, and open the file config.js with a plain text editor. 🪟 Windows: right-click → Open with → Notepad. 🍎 macOS: right-click → Open With → TextEdit.
- Change the two addresses to Kindie's address — not the app's. Change only the text between the quotes, and put no
/ at the end. Both lines get the same address: pictures and files are served by Kindie itself.
apiUrl: "https://kindie.myschool.com",
fileUrl: "https://kindie.myschool.com",

dist/config.js with both addresses changed to Kindie's address.
- Leave the
firebase and vapidKey lines as they are. They are for notifications (push notifications) and can be filled in later.
- Save and close.
Step 3 — Copy the app to the server
On your computer.
You need a free program that copies files to a server — the one you used for Kindie: WinSCP on Windows (winscp.net), FileZilla on macOS and Linux (filezilla-project.org, choose FileZilla Client).
First, prepare the folder to copy. The package folder also contains the app's source code. The server does not need it, and if you ever ran the build tools on this computer the folder can hold gigabytes (node_modules). So do not copy the whole package folder:
- On your computer, create a new empty folder named exactly
kindie-app.
- Copy these three things from the package into it: the folder
dist and the two files docker-compose.prod.yml and Caddyfile.

The same preparation with commands (macOS/Linux): kindie-app contains Caddyfile, dist and docker-compose.prod.yml.
Then continue with this new kindie-app folder.
- Open the program and connect to your server with its IP address, user name and password:
- WinSCP: in the login window choose SFTP, fill in Host name (your server's IP address), User name and Password, then click Login.
- FileZilla: in the bar at the top, fill in Host as
sftp:// followed by your server's IP address (e.g. sftp://203.0.113.10), Username and Password, then click Quickconnect.

FileZilla: the bar at the top filled in with Host (sftp:// and the server address), Username and Password.
Asked whether to trust the server? Answer Yes / OK.

The first time, FileZilla asks whether to trust the server: click OK.
- The window has two halves: your computer on the left, the server on the right. The right half shows your "home" folder on the server, where the
kindie folder already is. Do not open any folder there.

Connected: your computer on the left, the server on the right (your home folder).
- Drag the
kindie-app folder you prepared from the left and drop it on an empty spot of the right half.

After the drag: the folder kindie-app is now on the server (right) and the queue is empty.
- In the right half, check that it is named exactly
kindie-app, in lowercase (if not: right-click → Rename).
✅ Check: on the server you now see two folders side by side: kindie and kindie-app. Open kindie-app: docker-compose.prod.yml and the folder dist are directly inside it. (In a terminal connected to the server, as in Step 4, ls -d kindie kindie-app and ls kindie-app show the same.)

Double-click kindie-app on the right: docker-compose.prod.yml, Caddyfile and dist are inside it.

On the server: the folders kindie and kindie-app, and the three items inside kindie-app.
Step 4 — Start the app
On the server.
- Open a terminal on your computer. 🪟 Windows: press Start, type
powershell, press Enter. 🍎 macOS: press ⌘ + Space, type Terminal, press Enter. 🐧 Linux: press Ctrl + Alt + T.
- Connect to the server. The line is
ssh, a space, your user name, @, your server's IP address, with no spaces around the @. With the made-up values root and 203.0.113.10 it looks like this — type your own instead, then press Enter:
ssh root@203.0.113.10
In the screenshots below the user name is ubuntu and the address is replaced by the example address.

The first connection asks whether to trust the server: type yes.

After yes, the window asks for the password. Nothing appears while you type.
| The window shows | You do |
Are you sure you want to continue connecting (yes/no/[fingerprint])? | Type yes and press Enter. |
A line ending in password: | Type your server password and press Enter. Nothing appears while you type — that is normal. |
Permission denied, please try again. | Wrong password or user name. Type the password again, carefully. |
Connection timed out or Connection refused | The IP address is mistyped, the server is switched off, or the connection port (22) is closed at your provider. |
Permission denied (publickey) | The server accepts only a key file, not a password. On your provider's website, set a password for the server (often called "Reset root password"), then connect again. |
ssh "is not recognized" (old Windows) | Install PuTTY, open it, type your IP address in Host Name, click Open, then enter your user name and password when asked. |

Connected: the line you type on now starts with the server's name.
✅ Check: The start of the line you type on shows the server's name, such as root@my-server. From now on, what you type in this window runs on the server. To paste a line: 🪟 Windows right-click in the window, 🍎 macOS press ⌘ + V, 🐧 Linux press Ctrl + Shift + V.
- Go into the app folder and start the app. Paste one line at a time and press Enter after each. If it asks for a password, type your server password — nothing appears while you type:
cd ~/kindie-app
sudo docker compose -f docker-compose.prod.yml up -d

The app container is started.
✅ Check: the last line says Started or Running. Nothing is built, so this takes less than a minute.
The app is running now, but nobody can reach it yet — Kindie's web server does not know its address. That is the next step.
Step 5 — Give Kindie the app's address
On the server.
Note: This step edits Kindie's own settings file, which contains private values, so it has no screenshot here. The text below says exactly what you will see.
- First repeat the check from Step 1. Do not continue until dnschecker shows your server's IP address.
- Go into the Kindie folder and open its settings file:
cd ~/kindie
nano kindie.env
✅ Check: you see a file full of settings, starting with lines that begin with #. The window is empty instead? You are in the wrong folder, or Kindie is installed in another place. Press Ctrl + X (if it asks Save modified buffer?, press N) and stop here — do not type anything into an empty file. Write to support: the folder is not where this guide expects it.
- Move down with the arrow keys (the mouse does not work here) to this line, near the end. (The line is not there? Go to the very end of the file and type the line of item 4 as a new line.)
# KINDIE_APP_ADDRESS=https://app.school.example.com
- Delete the
# (the # and the one space after it) and replace the address with your app's address — not the example below. Keep https://, and put no spaces around the =:
KINDIE_APP_ADDRESS=https://app.myschool.com
- Save with Ctrl + O, then Enter. Close with Ctrl + X. On a Mac these are also the Control key, not ⌘. Changed something by mistake? Press Ctrl + X, then N: nothing is saved, and you can open the file again.
- Apply the change. Kindie restarts during this: the admin site is unavailable for about a minute, and people stay signed in.
cd ~/kindie
sudo docker compose -f docker-compose.prod.yml up -d
✅ Check: wait one minute, then open Kindie's address (the admin site) in your browser. It loads as before. It does not? Open the file again (items 2 to 5), put the # back in front of the line, and apply again — then look in troubleshooting.
Step 6 — Open the app
On your computer or your phone.
Open the app's address (https://app.myschool.com) in the browser. If the page doesn't load yet, wait two minutes and refresh.

The Kindie sign-in screen served by the production server.
✅ Check: you see the Kindie sign-in screen, and a padlock next to the address. Sign in with a parent, teacher or driver account — school staff create these accounts in the Kindie admin site.
On a phone, add the app to the home screen so that it opens like a normal app: iPhone (Safari): Share → Add to Home Screen. Android (Chrome): menu ⋮ → Add to Home screen / Install app.
🎬 Video: Put the web app online with Docker (macOS to a server)
The whole installation from start to finish, without sound. English captions are on the screen (about 3 min).
🔁 Everyday use
The app keeps running by itself, also after the server restarts. It stores no data of its own: everything is on Kindie, so there is nothing to back up here.
Tip: Where to type commands. On the server, in the app folder: connect to the server, then type cd ~/kindie-app and press Enter.
| To… | Run |
| Check that the app is running | sudo docker compose -f docker-compose.prod.yml ps — the line should say Up |
| Stop the app | sudo docker compose -f docker-compose.prod.yml stop |
| Start it again | sudo docker compose -f docker-compose.prod.yml up -d |

docker compose ps: the app container is Up.
✏️ Change a setting
All the app's settings are in dist/config.js on the server.
nano dist/config.js
Make your change, save with Ctrl + O, then Enter, and close with Ctrl + X. There is nothing to restart on the server: the change shows the next time someone reloads the app. (The restart note in Settings (dist/config.js) is only for Docker on your own computer.)
Kindie's address changed? Put the new address in apiUrl and fileUrl. The app's address changed? Point the new address to the server (Step 1), then change the line KINDIE_APP_ADDRESS in Kindie's kindie.env and apply it (Step 5).
⬆️ Install a new version
- Unzip the new version on your computer. If a
kindie-app folder from last time is still there, delete it or rename it. Prepare the clean kindie-app folder again (dist, docker-compose.prod.yml, Caddyfile — see Step 3).
- In WinSCP / FileZilla, drag
kindie-app/dist/config.js from the server (right half) to your computer, into the dist folder of the new version, replacing the one there. This keeps your settings.
- Still in WinSCP / FileZilla, in the right half: delete the folder
kindie-app-old if one is left from an earlier update, then rename kindie-app to kindie-app-old. Copy the new folder to the server and rename it to kindie-app (Step 3).
- On the server, in a terminal (connect as in Step 4.1 to 4.2), run:
cd ~/kindie-app
sudo docker compose -f docker-compose.prod.yml up -d --force-recreate

The container is recreated with the new files.
✅ Check: open the app. Still the old version? Close every tab of the app (on a phone: close the app completely) and open it again.
The app does not open any more? Rename kindie-app to kindie-app-failed and kindie-app-old back to kindie-app, run the same two lines again, and write to support.
🩹 Having trouble?
Support: zinisoft.net@gmail.com
| Problem | What to do |
network kindie_default declared as external, but could not be found | Kindie is not running on this machine. Either Kindie is not installed on this server yet — install it first — or you are typing on your own computer instead of the server. Connect to the server and run the command there. |
permission denied while trying to connect to the docker API | The command is missing sudo at the start. |
docker: command not found on the server | Kindie was not installed with Docker on this server. Use 4B instead. |
cd ~/kindie-app says No such file or directory | The folder on the server has another name or is inside another folder. Fix it in WinSCP / FileZilla (Step 3). |
| The app's address says not secure, or can't be reached | The address doesn't point to the server yet (redo the check in Step 1), or the line KINDIE_APP_ADDRESS in Kindie's kindie.env is still switched off (it starts with #), misspelled, or says http:// (Step 5). Wait 10 minutes after fixing it. |
| dnschecker shows the right IP address, but your browser still can't open the app | Your computer still remembers the old place. Close the browser completely and open it again, or try a private (incognito) window, or a phone on mobile data. |
The app's address shows an empty white page, or 502 | The app is not running. Run the "check that the app is running" command (everyday use), then start it. |
An error mentioning kindie.env after Step 5 | The line you changed is not written NAME=value. It must read KINDIE_APP_ADDRESS=https://… with no spaces around the =. |
The app shows a white page right after you changed config.js | A quote " or a comma was removed while editing. Open the file again (Change a setting): every line inside the braces looks like name: "value",. |
| The app opens but signing in fails | apiUrl in dist/config.js must be Kindie's address, exactly as you open the admin site, with https:// and no / at the end. Also check that the admin site itself opens. |
| Photos or files do not show | fileUrl in dist/config.js must be the same address as apiUrl. |
You changed config.js but nothing changed | You edited the copy on your computer. The real one is on the server (Change a setting). Then reload with Ctrl + F5 (macOS: ⌘ + Shift + R). |
| The app never asks to allow notifications | The firebase values in dist/config.js are still empty, or the app is not opened over https://. On an iPhone, open it from the Home Screen. |
| Allowed notifications, but nothing arrives | Kindie's key file and the app's web settings come from two different Firebase projects, or the web key does not allow the app's address (push notifications, step 3). More cases: the troubleshooting table in PUSH-NOTIFICATIONS.md. |
🗑️ Uninstall
On the server:
cd ~/kindie-app
sudo docker compose -f docker-compose.prod.yml down

docker compose down removes the app container.
Then, in Kindie's kindie.env, put the # back in front of the line KINDIE_APP_ADDRESS and apply the change (Step 5), and delete the kindie-app folder. Kindie and its data are not touched.
🛠️ Production without Docker (manual, Caddy)
Applies to Kindie App 5.0.0 and newer, together with Kindie 5.0.0 and newer. This part puts the Kindie App on the Internet at its own address, such as https://app.myschool.com, so that parents, teachers and drivers can open it on their phones. The app gets and renews its HTTPS certificate (the padlock in the browser) by itself. It is for a server where Kindie does not run with Docker — or for a separate server that runs only the app. Here you install the one program the app needs, a web server, directly on the server.
You do not need to be technical. Every step tells you where to click or what to paste, and ends with a ✅ check so you know it worked before you go on. Only want to try the app on your own computer? Use Run the app on your computer.
✅ What you need
| Item | Details |
| Kindie, already live | The app is only the screen: all data comes from Kindie (the package "Kindie — Kindergarten Management"). Kindie must already be on the Internet at its own address starting with https://, for example https://kindie.myschool.com, with its setup wizard finished. |
| A server | A Linux server running Ubuntu 22.04 or 24.04. The server that runs Kindie is fine — the app is small — as long as Kindie was installed there without Docker. A separate small server works too. |
| A domain name | The domain you already use for Kindie, and the login to the website where you bought it. |
| The Kindie App package | This folder, unzipped on your computer. |
Important: Kindie runs with Docker on that server? Then use
4A instead. The web server of this part and Kindie's Docker web server cannot both use the same server.
Two addresses — don't mix them up
| Address | Example | Who uses it |
| Kindie's address | https://kindie.myschool.com | School staff, for the admin site. You already have it. |
| The app's address | https://app.myschool.com | Parents, teachers and drivers. You create it in Step 2. |
Two machines, two places to work
- Your computer — the one in front of you, with a screen and a web browser. You edit a file, copy the app folder and open the app in the browser here.
- The server — the rented machine. It has no screen. You give it commands by typing them in a terminal on your computer that is connected to it (Step 4).
📚 Words used in this guide
- IP address — the server's number on the Internet, four numbers such as
203.0.113.10.
- Terminal — a window where you type a command and press Enter to run it. A command is finished when a new empty line appears and you can type again. While you paste a block of several lines, lines starting with
> flash by — that is normal. Stuck afterwards — the line you type on stays at >, or nothing happens for minutes? Press Ctrl + C and paste the whole block again.
- Web server — the free program, named Caddy, that hands the app to the phones and provides HTTPS. You install it in Step 7.
Note: Screenshots in this part were taken on macOS (Terminal and FileZilla). On Windows, PowerShell and WinSCP work the same way. The addresses in the screenshots are replaced by the examples above (203.0.113.10, kindie.myschool.com, app.myschool.com). The windows of your server provider and the DNS website are not shown: the text explains them.
📦 Install
Nine steps, about 40 minutes. Do the steps in order — you never have to go back.
Step 1 — Write down your server's login
You need three things. Write them down; almost every step uses them.
| What | Where to find it |
| IP address | On your server provider's website: sign in, open the list of servers, click your server. Look for IP address, Public IP or IPv4. It is also in the email the provider sent when the server was created. |
| User name | In the same place or the same email. Often root, sometimes ubuntu. |
| Password | The one you chose when creating the server, or the one in that email. |
Important: The IP address must be a public one. If it starts with 192.168. or 10., it only works inside one office or home network: phones on the Internet cannot reach the server and the app cannot get its HTTPS certificate.
The examples in this part are not yours. To show what a line looks like, this part uses made-up values. Wherever you see one, type your own instead:
| In this part | It stands for | Yours (write it here) |
203.0.113.10 | your server's IP address | |
root | your server user name | |
kindie.myschool.com | Kindie's address — the one you open the admin site with | |
app.myschool.com | the app's address (you choose it in Step 2) | |
Everything else in a command is typed exactly as shown, except where a step tells you to replace something.
Step 2 — Point the app's address to the server
On your computer, in the browser.
- Sign in to the website where you bought your domain and open its DNS settings (the list that says where each address goes).
- Add one new record. Do not change the records that already exist.
| Type | Name | Value |
A | app — only the first word of the app's address, without your domain | the IP address from Step 1 |
- Save.
Tip: Is your domain managed at Cloudflare? Set Proxy status to DNS only — click the orange cloud so that it turns grey. With the orange cloud, the app cannot get its HTTPS certificate.
✅ Check: open dnschecker.org, type the app's address without https:// (e.g. app.myschool.com), choose type A, and search. You should see your server's IP address. This can take from a few minutes to a few hours. You can do Steps 3 to 7 while you wait, but not Step 8.
Step 3 — Open the server's doors
On your computer, in the browser.
Kindie already runs on this server? Then this is done already — go to Step 4.
Phones reach your server through two "ports", numbered 80 and 443. Both must be open. Sign in to your server provider's website and find the Firewall page for your server (some providers call it "Security group"). Allow incoming TCP 80 and TCP 443. Keep TCP 22 open too — you need it to connect to the server.
A rule usually has three boxes. Fill them in like this, once for each port:
| Protocol | Port | Source (who may come in) |
TCP | 80 | Anywhere — also written 0.0.0.0/0 |
TCP | 443 | Anywhere — also written 0.0.0.0/0 |
✅ Check: the list of rules shows ports 22, 80 and 443. Whether they really work shows in Step 8, when the page opens. No firewall page at your provider? Then the ports are usually open already. Continue.
Step 4 — Connect to the server
On your computer, in a terminal.
- Open a terminal on your computer. 🪟 Windows: press Start, type
powershell, press Enter. 🍎 macOS: press ⌘ + Space, type Terminal, press Enter. 🐧 Linux: press Ctrl + Alt + T.
- Connect to the server. The line is
ssh, a space, your user name, @, your server's IP address, with no spaces around the @. With the made-up values root and 203.0.113.10 it looks like this — type your own instead, then press Enter:
ssh root@203.0.113.10
In the screenshots below the user name is ubuntu and the address is replaced by the example address.
- The window now asks up to three things, in this order:

The first connection asks whether to trust the server: type yes.

After yes, the window asks for the password. Nothing appears while you type.
| The window shows | You do |
Are you sure you want to continue connecting (yes/no/[fingerprint])? | Type yes and press Enter. |
A line ending in password: | Type your server password and press Enter. Nothing appears while you type — that is normal. |
Permission denied, please try again. | Wrong password or user name. Type the password again, carefully. |
Connection timed out or Connection refused | The IP address is mistyped, the server is switched off, or the connection port (22) is closed at your provider. |
Permission denied (publickey) | The server accepts only a key file, not a password. On your provider's website, set a password for the server (often called "Reset root password"), then connect again. |
ssh "is not recognized" (old Windows) | Install PuTTY, open it, type your IP address in Host Name, click Open, then enter your user name and password when asked. |

Connected: the line you type on now starts with the server's name.
✅ Check: The start of the line you type on shows the server's name, such as root@my-server. From now on, what you type in this window runs on the server. To paste a line: 🪟 Windows right-click in the window, 🍎 macOS press ⌘ + V, 🐧 Linux press Ctrl + Shift + V.
Tip: How to know where you are typing. Look at the start of the line. The server's name means you are on the server. Your own computer's name (or PS C:\Users\… on Windows) means you are on your computer — connect again from step 2.
Leave this terminal window open. You need it again in Step 7.
Step 5 — Tell the app where Kindie is
On your computer.
Do the same as Step 2 — Tell the app where Kindie is: open dist/config.js and change apiUrl and fileUrl to Kindie's address — not the app's — with no / at the end. Leave the firebase and vapidKey lines as they are (they are for notifications, push notifications). Save and close.
apiUrl: "https://kindie.myschool.com",
fileUrl: "https://kindie.myschool.com",

dist/config.js with both addresses changed to Kindie's address.
Step 6 — Copy the app to the server
On your computer.
You need a free program that copies files to a server:
| Your computer | Program | Where to get it |
| 🪟 Windows Windows | WinSCP | winscp.net |
| 🍎 macOS macOS | FileZilla | filezilla-project.org — choose FileZilla Client. If the installer offers other software, click Decline. |
| 🐧 Linux Linux | FileZilla | Your software store, or run sudo apt install filezilla |
First, prepare the folder to copy. The package folder also contains the app's source code. The server does not need it, and if you ever ran the build tools on this computer the folder can hold gigabytes (node_modules). So do not copy the whole package folder:
- On your computer, create a new empty folder named exactly
kindie-app.
- Copy the folder
dist from the package into it. Nothing else is needed.

The same preparation with commands (macOS/Linux): kindie-app contains only dist.
Then continue with this new kindie-app folder.
- Open the program and connect to your server with the three things from Step 1:
- WinSCP: in the login window choose SFTP, fill in Host name (your server's IP address), User name and Password, then click Login.
- FileZilla: in the bar at the top, fill in Host as
sftp:// followed by your server's IP address (e.g. sftp://203.0.113.10), Username and Password, then click Quickconnect.

FileZilla: the bar at the top filled in with Host (sftp:// and the server address), Username and Password.
Asked whether to trust the server? Answer Yes / OK — this happens the first time.

The first time, FileZilla asks whether to trust the server: click OK.
- The window has two halves: your computer on the left, the server on the right. The right half already shows the right place — your "home" folder on the server (
/root, or /home/ followed by your user name). Do not open any folder there.

Connected: your computer on the left, the server on the right (your home folder).
- Drag the
kindie-app folder you prepared from the left and drop it on an empty spot of the right half. Wait until the copy is finished.

After the drag: the folder kindie-app is now on the server (right) and the queue is empty.
- In the right half, check that the folder is named exactly
kindie-app, in lowercase (if not: right-click → Rename).
✅ Check: open kindie-app on the server: the folder dist is directly inside it, not inside a second folder.

Double-click kindie-app on the right: the folder dist is directly inside it.
Step 7 — Install the web server and put the app in place
On the server — the terminal from Step 4. Closed it? Open a terminal and connect again (Step 4.1 to 4.3).
Paste each dark block as a whole and press Enter, then wait until you can type again before pasting the next one. To paste: 🪟 Windows right-click in the window (it may warn that you are pasting several lines: choose Paste anyway), 🍎 macOS press ⌘ + V, 🐧 Linux press Ctrl + Shift + V. If a pink or purple screen appears, press Enter. If the server asks for a password, type your server password.
First, let the server learn your password once — otherwise a password question can swallow the next line you paste. Type sudo -v, press Enter, type your server password if asked (nothing shows as you type), and press Enter. Then paste the blocks below. (If you are connected as root, this step is not needed.)
sudo -v

sudo -v asks for the server password once (nothing shows while you type).
- Install the web server, Caddy. It is already on this server because Kindie runs here? Paste the block anyway — it changes nothing then.
sudo apt update && sudo apt install -y curl gnupg debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
sudo systemctl enable --now caddy
Important: If apt answers 402 Payment Required for dl.cloudsmith.io (the Caddy package repository refuses downloads), install Caddy from its official release page instead. Paste this block (it picks the latest version for your server's type of processor):
ARCH=$(dpkg --print-architecture)
URL=$(curl -s https://api.github.com/repos/caddyserver/caddy/releases/latest | grep -o "https://[^\"]*linux_${ARCH}\.deb" | head -1)
curl -fL -o /tmp/caddy.deb "$URL"
sudo apt install -y /tmp/caddy.deb
sudo systemctl enable --now caddy
✅ Check: run systemctl is-active caddy. It answers active.
- Put the app in the folder the web server reads from:
sudo mkdir -p /var/www
sudo rm -rf /var/www/kindie-app
sudo cp -r ~/kindie-app/dist /var/www/kindie-app
sudo chmod -R a+rX /var/www/kindie-app

The app is copied to /var/www/kindie-app; ls shows index.html and config.js among its files.
✅ Check: run ls /var/www/kindie-app. The answer contains index.html and config.js.
Note: From now on, the copy in /var/www/kindie-app is the real one. The folder kindie-app in your home folder is no longer used; you may delete it.
- Give the web server its instructions. Paste the whole block below, from the first line down to the last, and press Enter. There is nothing to change in it.
sudo mkdir -p /etc/caddy/sites /etc/systemd/system/caddy.service.d
# Keep a copy of the current Caddyfile, then ADD one line to it (never replace it), so any
# other site already served by this web server keeps working.
[ -f /etc/caddy/Caddyfile ] && sudo cp -n /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-kindie-app
grep -qs 'import /etc/caddy/sites/' /etc/caddy/Caddyfile || printf '\nimport /etc/caddy/sites/*.caddy\n' | sudo tee -a /etc/caddy/Caddyfile
sudo tee /etc/caddy/sites/kindie-app.caddy > /dev/null <<'EOF'
# Kindie App — written by PRODUCTION-MANUAL.md. The address comes from /etc/caddy/kindie-app.env.
{$KINDIE_APP_ADDRESS} {
root * /var/www/kindie-app
encode gzip zstd
route {
# Always check for the current file: a change in config.js or a new version shows
# up on the next reload — except files whose name contains a hash of their content.
header Cache-Control "no-cache"
header /_expo/static/* Cache-Control "public, max-age=31536000, immutable"
# The app handles its own page addresses: an unknown path gets index.html
try_files {path} /index.html
file_server
}
}
EOF
echo 'KINDIE_APP_ADDRESS=https://app.school.example.com' | sudo tee /etc/caddy/kindie-app.env
sudo tee /etc/systemd/system/caddy.service.d/kindie-app.conf > /dev/null <<'EOF'
[Service]
EnvironmentFile=/etc/caddy/kindie-app.env
EOF
sudo systemctl daemon-reload

The block as it looks in the terminal: lines starting with > are normal, and the line import /etc/caddy/sites/*.caddy appears.
When you paste the block, the screen shows a line import /etc/caddy/sites/*.caddy. That is normal: it is the line being added to the web server's main settings file. On Ubuntu 24.04 it may also show cp: warning: behavior of -n is non-portable… — harmless, the copy was made.
✅ Check: run grep import /etc/caddy/Caddyfile. The answer contains the line import /etc/caddy/sites/*.caddy. (Check 1)
✅ Check: run cat /etc/caddy/kindie-app.env. It answers KINDIE_APP_ADDRESS=https://app.school.example.com — a placeholder address that you replace in the next step. (Check 2)

Check 1 and check 2 give the expected answers.
Kindie runs on this server too? The block only adds one line to the web server's settings and first keeps a copy of the old file as /etc/caddy/Caddyfile.before-kindie-app, so Kindie's own settings stay. Not sure how Kindie's web server was installed (for example, you did not install it with the Kindie guide)? Write to support before this step.
Does Kindie need to be told the app's address? No. In our tests the Kindie server accepted requests from any web address, so you do not change anything on the Kindie side. Only if sign-in fails from the app's address while it works from Kindie's own address, write to support.
Step 8 — Set the app's address and turn it on
On the server.
- First repeat the check from Step 2. Do not continue until dnschecker shows your server's IP address — otherwise the app cannot get its HTTPS certificate.
- Open the app's address file in the server's text editor:
sudo nano /etc/caddy/kindie-app.env

The editor (nano) shows the one line of the file, with the placeholder address.
- The file has one line. Move with the arrow keys (the mouse does not work here) and replace
app.school.example.com with the app's address from Step 2. Type it in lowercase, with no / at the end. Keep KINDIE_APP_ADDRESS=https:// in front, with no spaces. Result:
KINDIE_APP_ADDRESS=https://app.myschool.com

The line after the change.
- Save with Ctrl + O, then Enter. Close with Ctrl + X. On a Mac these are also the Control key, not ⌘. Changed something by mistake? Press Ctrl + X, then N: nothing is saved, and you can open the file again.
- Apply:
sudo systemctl restart caddy
✅ Check: run systemctl is-active caddy. It answers active. If Kindie runs on this server too, open Kindie's address in your browser: the admin site loads as before.

The web server is restarted and answers active.
Important: It answers failed? Then the line you typed has a mistake, and every site on this server is offline until it is fixed — also Kindie. Do items 2 to 5 again right away: the line must read KINDIE_APP_ADDRESS=https:// followed by the app's address, and nothing else. Still failed? Bring Kindie back first, with these two lines, then write to support: sudo rm /etc/caddy/sites/kindie-app.caddy and sudo systemctl restart caddy.
Step 9 — Open the app
On your computer or your phone.
Open the app's address (https://app.myschool.com) in the browser. If the page doesn't load yet, wait two minutes and refresh.

The Kindie sign-in screen served by the web server.
✅ Check: you see the Kindie sign-in screen, and a padlock next to the address. Sign in with a parent, teacher or driver account — school staff create these accounts in the Kindie admin site.
On a phone, add the app to the home screen so that it opens like a normal app: iPhone (Safari): Share → Add to Home Screen. Android (Chrome): menu ⋮ → Add to Home screen / Install app.
🎬 Video: Put the web app online without Docker (macOS to a server)
The whole installation from start to finish, without sound. English captions are on the screen (about 4 min).
🔁 Everyday use
There is nothing to start or stop: the app is a set of files that the web server hands out. It is available whenever the server runs, also after a restart. It stores no data of its own — everything is on Kindie, so there is nothing to back up here.
Tip: Where to type commands. On the server: open a terminal and connect (Step 4.1 to 4.3). Paste the commands one line at a time.
| To… | Run |
| Check that the web server is running | systemctl is-active caddy — it answers active |
| See what is happening (errors) | sudo journalctl -u caddy -n 50 |
✏️ Change a setting
The app has two files with settings, both on the server:
| What | File | After changing it |
| Kindie's address, notifications | /var/www/kindie-app/config.js | Nothing — the change shows the next time someone reloads the app. |
| The app's own address | /etc/caddy/kindie-app.env | Run sudo systemctl restart caddy. |
Open a file with sudo nano followed by a space and its name, for example:
sudo nano /var/www/kindie-app/config.js
Make your change, save with Ctrl + O, then Enter, and close with Ctrl + X. Kindie's address changed? Put the new address in apiUrl and fileUrl of config.js. The app's address changed? Point the new address to the server (Step 2), then change the address file and restart the web server (Step 8).
⬆️ Install a new version
- On your computer: unzip the new version, prepare a clean
kindie-app folder containing only dist (Step 6), and copy it to your home folder on the server. An old kindie-app folder is still there? Delete it first (right-click → Delete).
- On the server: check that the new folder arrived. Run:
ls ~/kindie-app/dist/index.html
✅ Check: the answer ends with dist/index.html. It answers No such file or directory? Stop here and redo step 1 — nothing has been changed yet.
- Paste one line at a time. The first line keeps your settings; the others set the running app aside as
kindie-app-old and put the new version in its place:
sudo cp /var/www/kindie-app/config.js ~/kindie-app/dist/config.js
[ -f ~/kindie-app/dist/index.html ] && [ -f /var/www/kindie-app/index.html ] && sudo rm -rf /var/www/kindie-app-old
sudo mv /var/www/kindie-app /var/www/kindie-app-old
sudo cp -r ~/kindie-app/dist /var/www/kindie-app
sudo chmod -R a+rX /var/www/kindie-app
✅ Check: open the app. Still the old version? Close every tab of the app (on a phone: close the app completely) and open it again.
The app does not open any more? Put the previous one back with sudo rm -rf /var/www/kindie-app and then sudo mv /var/www/kindie-app-old /var/www/kindie-app, and write to support.
🩹 Having trouble?
Support: zinisoft.net@gmail.com. To send text from the terminal: select it with the mouse, then press Enter (Windows), ⌘ + C (macOS) or Ctrl + Shift + C (Linux) to copy it, and paste it into your email. A screenshot works too.
While installing
| Problem | What to do |
Could not get lock /var/lib/dpkg/lock-frontend | A new server is still installing its own updates. Wait five minutes and paste the same block again. |
Permission denied or are you root? | The line is missing sudo at the start. Paste the block again, complete. |
command not found for apt, systemctl or sudo, on a line that starts with your computer's name | You are typing on your own computer, not on the server. Connect to the server (Step 4.1 to 4.3) and run the command there. |
cp: cannot stat '/root/kindie-app/dist' (or /home/…/kindie-app/dist) | The folder on the server has another name or is inside another folder. Fix it in WinSCP / FileZilla (Step 6), then paste the block of Step 7.2 again. |
apt answers 402 Payment Required for dl.cloudsmith.io | The Caddy package repository refuses downloads. Use the block under Step 7.1 that installs Caddy from its release page. |
After Step 7.1, caddy does not answer active | Another web server already uses ports 80 and 443. First: if Kindie runs with Docker on this server, stop here and use 4A instead. ⚠️ If the server runs another website, the next command takes that website offline — write to support instead. Otherwise run sudo systemctl disable --now nginx apache2, then sudo systemctl restart caddy. |
After Step 8, caddy answers failed | The line in the address file has a space or a typing mistake. It must read KINDIE_APP_ADDRESS=https://… and nothing else. Correct it and apply again (Step 8). While it says failed, every site on this server is offline — also Kindie, if it runs here. |
Opening the app
| Problem | What to do |
| The app's address says not secure, or can't be reached | The address doesn't point to the server yet, the server's IP address is not a public one, or ports 80 and 443 are closed. Redo the checks in Steps 1 to 3, wait 10 minutes, and refresh. At Cloudflare, the cloud next to your record must be grey. |
Still can't be reached, and cat /etc/caddy/kindie-app.env shows app.school.example.com | The address was not saved. Do Step 8 again. |
| dnschecker shows the right IP address, but your browser still can't open the app | Your computer still remembers the old place. Close the browser completely and open it again, or try a private (incognito) window, or a phone on mobile data. |
The app's address shows an empty white page, or 404 | The app's files are not in /var/www/kindie-app. Paste the block of Step 7.2 again and do its check. |
| You see a page that says Caddy works | The web server has not received its instructions. Paste the block of Step 7.3 again, then do Step 8. |
The app shows a white page right after you changed config.js | A quote " or a comma was removed while editing. Open the file again (Change a setting): every line inside the braces looks like name: "value",. |
| The app opens but signing in fails | apiUrl in config.js must be Kindie's address, exactly as you open the admin site, with https:// and no / at the end. Also check that the admin site itself opens. |
| Photos or files do not show | fileUrl in config.js must be the same address as apiUrl. |
Using the app
| Problem | What to do |
You changed config.js but nothing changed | You edited the copy on your computer or the one in your home folder. The real one is /var/www/kindie-app/config.js (Change a setting). Then reload with Ctrl + F5 (macOS: ⌘ + Shift + R). |
nano says Permission denied or File is unwritable | You opened the file without sudo. Close with Ctrl + X and open it again with sudo nano in front. |
| The app never asks to allow notifications | The firebase values in config.js are still empty, or the app is not opened over https://. On an iPhone, open it from the Home Screen. |
| Allowed notifications, but nothing arrives | Kindie's key file and the app's web settings come from two different Firebase projects, or the web key does not allow the app's address (push notifications, step 3). More cases: the troubleshooting table in PUSH-NOTIFICATIONS.md. |
🗑️ Uninstall
Run on the server, one line at a time:
sudo rm /etc/caddy/sites/kindie-app.caddy /etc/caddy/kindie-app.env /etc/systemd/system/caddy.service.d/kindie-app.conf
sudo systemctl daemon-reload
sudo systemctl restart caddy
sudo rm -rf /var/www/kindie-app /var/www/kindie-app-old ~/kindie-app

The four uninstall lines, typed one at a time.
Kindie and its data are not touched. The web server itself stays installed. If Kindie runs on this server, leave it: Kindie needs it. Only on a server that ran nothing but the app, remove it with sudo apt purge -y caddy.
🔔 Push notifications on a server
Optional. Without it the app works normally; phones just receive no notifications. Notifications need two sides, set up with the same Firebase project:
| Side | What it needs | Where |
| Kindie (sends) | A private key file | PUSH-NOTIFICATIONS.md, section 5 |
| This app (receives) | The Firebase web settings and a "VAPID" key | dist/config.js (4A) or /var/www/kindie-app/config.js (4B) |
- Follow
PUSH-NOTIFICATIONS.md in this package, sections 2 and 3. You end up with a block of web settings and a VAPID key.
- On the server, put them into the app's settings file (
nano dist/config.js in 4A, sudo nano /var/www/kindie-app/config.js in 4B): the values go between the quotes inside firebase: { … }, one value per line, and the key between the quotes of vapidKey. Keep the quotes and the comma at the end of each line — a filled-in line looks like apiKey: "AIzaSyEXAMPLE-ONLY-not-a-real-key",.
- If you limited the web key to certain websites (section 7 of that guide), the list must contain the app's address, e.g.
https://app.myschool.com/*.
- Reload the app, sign in, and choose Allow when the browser asks about notifications.
✅ Check: in the Kindie admin site, send a chat message to the account that is signed in on the phone. The notification appears on the phone.
Tip: iPhone: Safari only allows notifications after the app was added to the Home Screen and opened from there. iOS 16.4 or newer is required.
🩹 Problems on your computer
| Problem | What to check |
| Port 8080 is already in use | Docker: in docker-compose.yml, change "127.0.0.1:8080:8080" to "127.0.0.1:8081:8080", run docker compose up -d, then open http://localhost:8081. start.bat: change -l 8080 to -l 8081 in the file and open http://localhost:8081. |
| The page opens, but signing in fails | The app does not know your server yet. Put your server's address in apiUrl and fileUrl in dist/config.js (Settings (dist/config.js)). On Docker, then run docker compose restart. |
The black start.bat window closes at once | Node.js is not installed, or the package was not unzipped. See Option 1. |
docker compose says it cannot connect to Docker | Docker Desktop is not running. Start it and wait until it says it is running. |
| A yellow "pull access denied for kindie-app" line appears when starting Docker | Harmless. Docker looks for a ready-made image first, then builds it from this package. |
node -v does not print v20.11.x (Option 3) | Install Node.js 20.11 as described in Option 3. Open a new terminal afterwards. |
| Android phone not found | Run adb devices. Check the USB cable, enable USB Debugging, and accept the "Allow USB Debugging" RSA prompt. More: Android: When something goes wrong. |
| iPhone app won't open | Open iPhone Settings → General → VPN & Device Management → tap your Apple ID → tap Trust. More: iPhone: When something goes wrong. |
| No push notifications | See the troubleshooting table in PUSH-NOTIFICATIONS.md. On iPhone they need the paid Apple account. More: Push notifications. |
Still stuck? Email zinisoft.net@gmail.com.
🩹 Problems after going live
| Problem | Cause and fix |
| The app opens but shows no data, or signing in fails | config.js still has the sample address http://localhost:1337, or apiUrl is not the exact address of your Kindie server. Put your own address in apiUrl and fileUrl (https://…, no / at the end), save, and press Ctrl + F5. No rebuild is needed. |
| Photos and files do not appear | fileUrl is wrong or cannot be reached. Set it to the address of your Kindie server (the one that serves the uploads). |
| Refreshing the page, or opening a link directly, shows 404 Not Found | The app is a single-page app, so the web server must send every unknown address to index.html instead of a 404 (often called "single-page app" or "fallback to index.html"). The Caddy setups in this guide already do this. |
| You uploaded a new version but people still see the old one | The browser keeps the old version in its service worker cache. Open the site, press F12 → Application → Service Workers → Unregister, then Clear site data and reload. Users can do the same, or close every tab of the app and open it again. |
| Chat messages only appear after refreshing the page | The WebSocket connection (/socket.io) is blocked, usually by a proxy that does not pass WebSocket through, or because the site is not on HTTPS. Allow WebSocket on that layer and serve everything over HTTPS (Caddy does by default). |
| No notification permission prompt, or the console says Firebase is not supported in this browser | Push needs HTTPS (the only exception is localhost), a current browser, and notifications allowed in the site's settings. |
| Notifications work while the app is open but not when it is closed | firebase-messaging-sw.js must sit at the top level of the uploaded dist/ folder, next to config.js, with that exact name, so that https://your-domain.com/firebase-messaging-sw.js opens. Do not rename it or move it into a subfolder. |
📚 Reference: which guide, what runs where
The Android and iPhone apps are built from this same package, on your own computer. Each job has its own step-by-step guide: Android app and iPhone app pages, and the files in the package:
| I want to… | Guide | You need |
| Run the app on my Android phone | Android app · docs/android-setup.md | Windows, macOS or Linux, with Android Studio |
| Run the app on my iPhone | iPhone app · docs/ios-setup.md | A Mac with Xcode. A free Apple ID is enough for testing |
| Give the app to parents (Google Play, TestFlight, App Store) | docs/publishing.md | Google Play account ($25 once), Apple Developer Program ($99 / year) |
| Test the finished app on a real phone | docs/ios-device-test-checklist.md | The checklist is for iPhone and Android |
All of them need Node.js 20.11, your own Firebase file and a .env.local file. The guides take you through each one.
Important: Make the apps yours before you publish. The package ships with a placeholder id (com.yourschool.kindie) and prints a warning at build time if you forget. Set your own app name, Bundle ID and Android package in .env.local, replace the icons, and use your own Firebase project (docs/publishing.md, section 1). The Bundle ID / package cannot be changed after you publish, so choose it once. The web app is not affected by any of this.
Important: When you install a new version of this package, compare your .env.local with the new .env.example: new settings may have been added.
🔔 Push notifications
Optional. Without it the app works normally; people just receive no notifications. Kindie sends notifications through Firebase Cloud Messaging (FCM), using your own Firebase project:
| Platform | Notifications |
| Web / PWA | ✅ Through the browser's service worker. Phones receive them once the app is live at an https:// address |
| Android app | ✅ Through the phone's native FCM registration token |
| iPhone app | Needs the paid Apple Developer account ($99 / year). There is no step-by-step guide for iPhone notifications yet |
Setup, testing and troubleshooting: see PUSH-NOTIFICATIONS.md in the package. On an iPhone, the web app only receives notifications after you add it to the Home Screen (Share → Add to Home Screen) and open it from there; the iPhone needs iOS 16.4 or newer.
🗺️ What runs where
| Environment | Platform | Manual / command line | Docker | Recommended |
| Local | PWA (Web) | ✅ yarn web | ✅ docker compose | Docker or yarn web |
| Local | Android | ✅ ./run-android.sh on macOS; yarn android on Windows/Linux | ❌ | Native tools (USB live-reload) |
| Local | iOS | ✅ ./run-ios.sh + Xcode | ❌ Apple restricted | Xcode / macOS native (Apple requirement) |
| Production | PWA (Web) | ✅ a Linux server with Caddy | ✅ docker-compose.prod.yml | Docker (shares HTTPS with the backend) |
Note: Why isn't iOS supported in Docker? Apple restricts Xcode, the iOS SDKs and device code signing to macOS. Docker containers run on Linux and cannot run Xcode or talk to physical iOS hardware. Why is Android recommended via the command line rather than Docker locally? Docker Desktop on macOS runs inside a lightweight virtual machine that cannot pass a USB cable through to an Android phone for real-time debugging (adb reverse).
📄 Other guides in the package
| Document | Purpose |
docs/android-setup.md | Run the app on your Android phone, step by step |
docs/ios-setup.md | Run the app on your iPhone, step by step (Mac + Xcode) |
docs/publishing.md | Make the app yours and publish it: Google Play, TestFlight, App Store |
docs/ios-device-test-checklist.md | Test checklist for a real phone (iPhone and Android) |
PUSH-NOTIFICATIONS.md | Server Firebase Admin SDK and Web Push key guide |
PRODUCTION-DOCKER.md | Live server deployment with Docker and Caddy |
PRODUCTION-MANUAL.md | Live server deployment without Docker (installs the Caddy web server) |
CHANGELOG.md | Version release notes and migration history |
Support email: zinisoft.net@gmail.com
Android App
Build the Kindie app on your own computer and run it on an Android phone or an emulator. Works on Windows, macOS and Linux.
This part takes you from an empty computer to the Kindie app running on your own Android phone, connected to your own Kindie server.
Note: What you are building here is a
test app for your own phone. To give the app to parents and teachers (an installable file, or Google Play), follow this part first, then
docs/publishing.md. The
web app (PWA) does not need any of this: see
Web app (PWA).
Time needed: about 1 hour the first time (most of it is downloading Android tools). Later runs take a few minutes.
✅ 0. Before you start — what you need
| You need | Why |
| A computer: macOS, Windows 10/11 or Linux, with 16 GB RAM and 20 GB free disk space | Building an Android app is heavy |
| An Android phone (Android 8.0 or newer) and a USB cable that carries data (some cables only charge) | To run the app |
| Your Kindie server address | The app talks to it. For a first test, a server on your own computer (http://localhost:1337) is fine |
A Firebase project of your own and its google-services.json file | The Android project cannot be created without it (see step 3). It is also used for push notifications |
What to download (once — step 1 explains each one):
Note: 🐧 Linux The commands in this part are the macOS ones adapted for Linux (other file paths, apt for Java).
Many steps below say "run this in a terminal, in the project folder". The project folder is the one that contains package.json: see how to open a terminal. Unzip the package to a short path first (for example C:\kindie on Windows) — long folder names make the Android build fail.
Files that start with a dot (.env.example, .env.local) are hidden in Finder and File Explorer. The commands in step 4 create and open them for you.
🧰 1. Install the tools (once)
1.1 Android Studio (provides the Android SDK)
- Download Android Studio from developer.android.com/studio and install it (accept the defaults).
- Open it once. Let the setup wizard finish downloading the SDK.
- Open Settings → Languages & Frameworks → Android SDK (on macOS: Android Studio → Settings).
- Tab SDK Platforms: tick Android 16 (API 36).
- Tab SDK Tools: tick Android SDK Build-Tools 36, Android SDK Command-line Tools (latest), Android SDK Platform-Tools, NDK (Side by side) (choose version 27.1.12297006 through Show Package Details).
- Click Apply.
- Write down the Android SDK Location shown at the top of that screen. You need it in step 1.4.
1.2 Java 17
The Android build needs Java (JDK) 17. Android Studio now ships with a newer Java (21), which is not what this project is tested with, so install 17 yourself:
- 🪟 Windows 🍎 macOS Download Temurin 17 (LTS) from adoptium.net (Windows: the
.msi; macOS: the .pkg for your chip) and install it with the defaults.
- 🍎 macOS With Homebrew:
brew install openjdk@17.
- 🐧 Linux Ubuntu / Debian:
sudo apt install openjdk-17-jdk.
1.3 Node.js 20.11 and Yarn
The app needs Node.js 20.11.x (other versions, including newer ones, cause build errors).
- 🍎 macOS Run
./setup-environment.sh from the project folder. It checks Node, Yarn and Java, runs yarn install for you, creates your .env.local file and fixes what it can. It does not install Android Studio or the Android SDK (step 1.1). Read what it prints.
- 🐧 Linux Install nvm, open a new terminal, then run
nvm install 20.11.0, nvm alias default 20.11.0 and npm install -g yarn. (setup-environment.sh runs only on macOS.)
- 🪟 Windows Download
node-v20.11.0-x64.msi from nodejs.org/en/download/archive/v20.11.0 (the main download page only offers newer versions, which do not work here), install it, then open a new PowerShell window and run the commands below.
node -v # must print v20.11.x
npm install -g yarn
yarn -v # must print 1.22.x
If PowerShell says "running scripts is disabled on this system", run this once and try again:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
🍎 macOS This is what the setup script looks like on macOS:

./setup-environment.sh starts and checks the operating system, Homebrew, Node.js, Yarn and Java.

The script finished: it installed the libraries, created .env.local and warns that the Firebase files are missing.
Tip: The switch to Node 20.11 only lasts while the script runs: if node -v in your own Terminal does not say v20.11.x, run nvm alias default 20.11.0 once and open a new Terminal window, otherwise the yarn ... commands you type yourself use the wrong Node (./run-android.sh switches by itself).
1.4 Tell your computer where Android is
- 🍎 macOS Add these lines to
~/.zshrc, then open a new Terminal (~/.zshrc is a hidden file: open -e ~/.zshrc opens it in TextEdit). ./run-android.sh sets these for you, but typing the commands yourself needs them.
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
Check it worked (in a new terminal):
adb --version # prints "Android Debug Bridge version ..."
java -version # prints "openjdk version "17.…" — if it says 21, JAVA_HOME points to the wrong Java

adb --version prints the Android Debug Bridge version and java -version prints openjdk 17.
If adb or java is "not found", step 1.4 was not applied: close the terminal, open a new one and retry.
📱 2. Prepare your phone (once)
Tip: No phone at hand? An emulator works too. In Android Studio, open Device Manager, create a virtual device (choose a system image that says Google APIs or Google Play), start it with ▶ and wait for its home screen. Then skip this step: adb devices lists it as emulator-5554, and steps 3 to 6 are the same as for a phone (the adb reverse lines in step 5 work on an emulator too). Tested on Windows and macOS with a Pixel 9 virtual device. An emulator is not a substitute for a real phone before you publish: camera, notifications and speed differ. The screenshots in this part were taken on an emulator.
- Open Settings → About phone and tap Build number 7 times. You are now a "developer". (Samsung: About phone → Software information → Build number.)
- Open Settings → System → Developer options and turn on USB debugging. On Xiaomi / Oppo / Realme / Vivo phones also turn on Install via USB.
- Connect the phone with the cable. On the phone, tap Allow USB debugging (tick Always allow).
- Check the computer sees it:
adb devices
🪟 Windows Some phones need the maker's USB driver (search "<your phone brand> USB driver"). You should see one line ending in device. If it says unauthorized, unlock the phone and accept the prompt. If the list is empty, try another cable or USB port.
🔥 3. Put your Firebase file in the project
The Android project is generated from app.config.ts, which requires google-services.json in the project's root folder.
First choose your package id (the app's unique name). Rule: your school's website name reversed, in lowercase letters and digits only, no spaces or dashes — for example edu.myschool.kindie (or com.myschoolname.kindie). You will type it in two places: Firebase (below) and .env.local (step 4). It cannot be changed after you publish.
- Go to console.firebase.google.com and create a project (or open yours).
- Project settings → Your apps → Add app → Android.
- For Android package name, type your package id. Register the app. (Google asks for a nickname and a SHA-1 key: leave both empty. Skip the Add Firebase SDK and Run your app pages: click Next, then Continue to console.)
- Download
google-services.json and copy it into the project's root folder (next to package.json).

ls shows google-services.json and package.json side by side in the project folder.
Important: The package name in Firebase and the one in your app must be identical, or the build stops with a Google Services error.
🔧 4. Tell the app who you are and where your server is
- In the project folder (terminal), create your settings file and open it. (If
.env.local already exists, the macOS command keeps it unchanged.)
- 🍎 macOS
cp -n .env.example .env.local && open -e .env.local
- 🐧 Linux
cp -n .env.example .env.local && nano .env.local (save with Ctrl+O, Enter, then Ctrl+X)
- 🪟 Windows (PowerShell)
copy .env.example .env.local ; notepad .env.local

The macOS command that creates .env.local and opens it in TextEdit.
- In the file that opened, change the lines to the values below, then save (Cmd+S / Ctrl+S). On a Mac, if TextEdit opens in rich text mode, choose Format → Make Plain Text first.
# Your server (see "Which server address?" below)
EXPO_PUBLIC_API_URL=http://localhost:1337
EXPO_PUBLIC_FILE_URL=http://localhost:1337
# Your own app identity
KINDIE_APP_NAME=My School
KINDIE_ANDROID_PACKAGE=edu.myschool.kindie

.env.local in TextEdit with the app name and the Android package changed.
Do not use com.kindie or com.yourschool.kindie: those are not yours. The build prints a warning if you forget.
Which server address?
| How you run the app | Value for EXPO_PUBLIC_API_URL / EXPO_PUBLIC_FILE_URL |
| Phone plugged in with USB, server on this computer | http://localhost:1337 — the script run-android.sh forwards the ports for you (macOS). On Windows run the two adb reverse commands in step 5 yourself |
| Phone on Wi-Fi, server on this computer | http://<your computer's IP>:1337. Find the IP: macOS System Settings → Wi-Fi → Details; Windows ipconfig (look for IPv4 Address). Phone and computer must be on the same Wi-Fi; turn off the phone's VPN |
| Server on the internet | https://kindie.myschool.com (your real address) |
- A release app (the one parents install) only accepts
https:// addresses. Plain http:// works for testing only.
- The server address is fixed into the app when it is built. If you change
.env.local, rebuild or restart as described in After you change settings.
▶️ 5. Run the app on your phone
🍎 macOS / Linux
./run-android.sh
The script checks adb, finds your phone, forwards the ports, creates the android/ folder the first time (this takes a few minutes), builds the app and installs it. 🐧 Linux On Linux, do step 1.4 first (ANDROID_HOME and JAVA_HOME), so that the script finds the Android SDK and Java 17.

./run-android.sh finds the device, forwards ports 8081 and 1337, creates the android/ project and starts the build.

The build is done: the app is installed and opened, and the code server prints Android Bundled.
🎬 Video: Install the Android app on macOS
The whole installation from start to finish, without sound. English captions are on the screen (about 2 min).
🪟 Windows (or if you prefer to type the commands)
In PowerShell, inside the project folder:
yarn install
adb reverse tcp:8081 tcp:8081 # lets the phone reach the development server
adb reverse tcp:1337 tcp:1337 # lets the phone reach a Kindie server on this computer
yarn android
- The first run creates the
android/ folder and compiles everything: 10–30 minutes is normal on a computer that has never built an Android app (it can be much faster when Gradle already has the files cached).
- When it finishes, the app opens on the phone. The first time, Expo shows a short developer menu introduction: tap Continue, then close the menu with the X. You then see the Kindie sign-in screen. Keep the terminal window open — it serves the app code while you test.
- A line
Could not parse Expo config: ios.googleServicesFile: "./GoogleService-Info.plist" may appear. It only says that the iPhone file is not there; ignore it for Android.

First launch: Expo's developer menu introduction with a Continue button.

The developer menu, connected to the code server. Close it with the X.

The Kindie sign-in screen on Android.
Using it later: if you only changed code (no new libraries), you do not rebuild. Run yarn start --dev-client and press a.
🔄 6. After you change settings
| You changed | Do this |
EXPO_PUBLIC_API_URL / EXPO_PUBLIC_FILE_URL | Stop the terminal (Ctrl+C), run yarn start --dev-client --clear, press a |
KINDIE_ANDROID_PACKAGE, KINDIE_APP_NAME, the icon, or anything in app.config.ts | Delete the android/ folder, uninstall the app from the phone, then run step 5 again |
google-services.json | Copy the new file into the project root, then delete android/ and run step 5 again |
🩹 7. When something goes wrong
| Message / symptom | What to do |
adb: command not found | Step 1.4 was not applied. Open a new terminal |
adb devices shows nothing or unauthorized | Re-plug the cable, unlock the phone, accept Allow USB debugging. Use a data cable |
SDK location not found | ANDROID_HOME is not set (step 1.4). Or create android/local.properties containing sdk.dir=<your SDK location> |
Could not determine the dependencies of task ':app:compile…' or Java errors | Wrong Java. java -version must say 17. Fix JAVA_HOME (step 1.4) |
| Node version errors | node -v must say v20.11.x (step 1.3) |
| Red screen "Unable to load script" | The development server is not reachable. Keep the terminal running; run adb reverse tcp:8081 tcp:8081; or put the phone on the same Wi-Fi as the computer |
| Login fails / "Network error" | The server address is wrong or unreachable from the phone — see the table in step 4. On USB, make sure adb reverse tcp:1337 tcp:1337 was run |
INSTALL_FAILED_UPDATE_INCOMPATIBLE | An older copy signed with another key is installed. Uninstall it from the phone, then run again |
| The first build is very slow | Normal. Gradle downloads many files. Do not interrupt it |
| The app stays on a loading screen the first time after the build | Normal. The code server (yarn start --dev-client) builds the app's code the first time the phone asks for it. Watch its terminal until it prints Android Bundled …. Later starts take seconds |
| "Cannot create the Android project: the Firebase file … is missing" | Step 3: put google-services.json in the project root, next to package.json, then run the command again. Nothing was created, so there is nothing to clean up |
| The app opens then closes at once | Run adb logcat -s ReactNativeJS:V ReactNative:V AndroidRuntime:E, reopen the app, and send the output to support |
| Xiaomi / Oppo / Realme / Vivo phone: the app is killed in the background | Turn off aggressive battery saving for the app and allow Autostart |
Still stuck? Email zinisoft.net@gmail.com with: your operating system, the output of node -v, java -version, adb devices, and the last 30 lines printed by the terminal.
📚 8. Reference
| Item | Value |
| Node.js / Yarn | 20.11.x / 1.22.x |
| Java (JDK) | 17 |
| Expo SDK / React Native | 53 / 0.79.6 |
compileSdk / targetSdk / minSdk | 36 / 36 / 26 (Android 8.0+) |
| Build-Tools / NDK / Gradle | 36.0.0 / 27.1.12297006 / 8.13 (downloaded automatically) |
| JavaScript engine | Hermes. The New Architecture is deliberately off — do not switch it on |
| Development server port | 8081 |
| Permissions used | Camera, microphone, notifications, internet. Storage permissions are removed on purpose |
| Android back gesture | Predictive back is switched off by a plugin, because React Native 0.79 does not support it yet |
Push notifications use Firebase. Setup and testing are described in PUSH-NOTIFICATIONS.md (see also Push notifications).
🛠️ Helper scripts (macOS)
These .sh scripts run on macOS. On Linux only ./run-android.sh works, after you set ANDROID_HOME and JAVA_HOME (Android: 4 Tell your computer where Android is); setup-environment.sh, start.sh and run-ios.sh stop on Linux. On Windows, follow Android app. They are for building the Android / iPhone apps; to just see the web app, use Run the app on your computer.
./start.sh
This opens a one-touch menu. Or use the direct Yarn shortcuts:
| Command | What it does |
yarn doctor | Run automated system health checks (Node, Java, CocoaPods, Firebase configs, devices). |
yarn setup:env | Checks your computer for the phone apps (Homebrew, Node 20.11 via nvm, Yarn, Java 17, CocoaPods) and installs what it can. It does not install Android Studio or Xcode. |
yarn run:android | Connect the Android phone, auto-configure adb reverse, and launch the app. |
yarn run:ios | Create the iOS project if needed, check CocoaPods, and open Xcode. |
iPhone App
Build the Kindie app on your Mac and run it on an iPhone or in the iOS Simulator (no iPhone needed to try it).
This part takes you from a Mac with nothing installed to the Kindie app running on your own iPhone, connected to your own Kindie server.
Note: What you are building here is a
test app for your own iPhone. To give the app to parents and teachers (TestFlight or the App Store), follow this part first, then
docs/publishing.md. iPhone apps can only be built on a
Mac (Apple's rule). The
web app (PWA) works on iPhone without any of this: see
Web app (PWA).
Time needed: about 1.5 hours the first time (Xcode alone is a 10+ GB download). Later runs take a few minutes.
Two ways to run it: No iPhone? You do not need one. Install Xcode and an
iOS Simulator runtime, then follow this part, except steps 2 and 5 (they are for a real iPhone), and run the app as described in the
Simulator appendix (step 8). No Apple ID is needed for the Simulator. The screenshots in this part were taken in the Simulator.
✅ 0. Before you start — what you need
| You need | Why |
| A Mac with Apple Silicon (M1 or newer) and 20 GB free disk space | Building iOS apps. Intel Macs have not been tested |
| An iPhone running iOS 15.1 or newer and its Lightning/USB-C cable | To run the app |
| An Apple ID (free) | To sign the app so it can run on your phone |
| Your Kindie server address | The app talks to it |
A Firebase project of your own and its GoogleService-Info.plist file | The iOS project cannot be created without it (step 3) |
What to download (once — step 1 explains each one):
| Tool | Version | Where | Installed by |
| Xcode (10+ GB) | 27.0 (see step 1) | Mac App Store | You |
| iOS Simulator runtime — only if you have no iPhone | the one offered by Xcode | Xcode → Settings → Components | You |
| Homebrew | current | brew.sh | setup-environment.sh, after you answer y |
| Node.js | 20.11.x exactly | nodejs.org/en/download/archive/v20.11.0 (or nvm) | setup-environment.sh if you have nvm; otherwise you |
| Yarn | 1.22.x | npm install -g yarn | setup-environment.sh |
| CocoaPods | 1.16.x | brew install cocoapods | setup-environment.sh |
Free Apple ID or paid Apple Developer Program ($99 / year)?
| What you get | Free Apple ID | Paid Developer Program |
| Test the app on your own iPhone (screens, camera, chat, server) | ✅ | ✅ |
| Push notifications | ❌ Apple does not allow them | ✅ (no step-by-step guide yet) |
| The test app stops working after | 7 days (run it again from Xcode) | 1 year |
| Give the app to parents (TestFlight / App Store) | ❌ | ✅ |
| Your own unique Bundle ID | ✅ | ✅ |
Your school's data is never lost when the 7-day test app expires: it lives on your server. Signing with your Apple ID only allows the app to run on your own phone; Xcode never reads your iCloud photos, messages or payment methods. Turning on Developer Mode and tapping Trust (step 5) is Apple's standard way to test an app: it does not void the warranty, does not weaken the phone's security and does not interfere with banking apps.
Important: Push notifications on iPhone need the paid account and there is no step-by-step guide for them yet. Everything else in the app works with a free Apple ID.
🧰 1. Install the tools (once)
- Xcode — install it from the Mac App Store (search "Xcode", about 10+ GB). Open it once, accept the license, and let it install the extra components it offers. If you will use the Simulator instead of an iPhone, also install an iOS Simulator runtime: Xcode → Settings → Components. Tested with Xcode 27.0 on macOS 27 (a Mac that cannot run macOS 27 cannot run this Xcode: the App Store would then offer an older Xcode, which has not been tested with this project). Building in the cloud with Expo EAS (publishing guide) has not been tested either.
- Open Terminal (Cmd+Space, type "Terminal", Enter) and go to the project folder. The easiest way: type
cd (with a space), then drag the unzipped project folder into the Terminal window and press Enter.
- Run the setup helper:
bash setup-environment.sh

The setup helper finished: libraries installed, .env.local ready and both Firebase files found.
It checks Homebrew, Node.js 20.11, Yarn, CocoaPods and your project settings, installs what it can, runs yarn install for you and creates your .env.local file. It may ask y/N questions and for your Mac password (nothing shows while you type). Read what it prints. Ignore the Java / Android warnings — they only matter for Android phones. If it says Node is missing and you do not have nvm, download node-v20.11.0.pkg from nodejs.org/en/download/archive/v20.11.0 (the main download page only offers newer versions, which do not work here), install it, open a new Terminal, and run it again.
Note: If macOS says "permission denied" for a .sh file, put bash in front of the command, as above.
Tip: The Node switch only lasts while the script runs. If your Terminal's own Node is not 20.11 (check with node -v), every yarn ... command you type yourself later (for example yarn start --dev-client in step 5) runs on the wrong Node. Fix it once with nvm alias default 20.11.0, then open a new Terminal window. Or run nvm use 20.11.0 in each new window. (./run-ios.sh switches by itself.)
- The script in step 3 already installed the project's libraries. Run
yarn install yourself only if the script stopped before it finished:
yarn install
Check it worked (in a new Terminal window):
echo $LANG # en_US.UTF-8 (if it prints nothing, see "CocoaPods requires your terminal to be using UTF-8" in step 7)
node -v # v20.11.x
yarn -v # 1.22.x
pod --version # 1.16.x
xcodebuild -version

The five check commands: LANG en_US.UTF-8, Node v20.11.0, Yarn 1.22.22, CocoaPods 1.16.2 and Xcode 27.0.
📱 2. Prepare your iPhone (once)
- Plug the iPhone into the Mac. On the phone tap Trust This Computer and enter your passcode.
- Developer Mode: on an iPhone that has never been used with Xcode, the switch may not exist yet. Look at Settings → Privacy & Security (scroll to the bottom). If you see Developer Mode, turn it on; the phone restarts. If you do not see it, skip this step: it appears after the first time Xcode installs the app (step 5) — then turn it on, let the phone restart, and press Play in Xcode again.
- Make sure the iPhone and the Mac are on the same Wi-Fi and the phone's VPN is off (the app loads its code from your Mac over Wi-Fi).
🔥 3. Put your Firebase file in the project
The iOS project cannot be generated without GoogleService-Info.plist in the project's root folder (this is why the build stops without it). You need a free Google account for Firebase; the free plan is enough.
- Go to console.firebase.google.com and create a project (or open yours).
- Project settings → Your apps → Add app → iOS.
- For Apple bundle ID, type your own Bundle ID. Choose it now: your school's website name reversed, lowercase letters and digits only, no spaces — for example
edu.myschool.kindie. (Do not copy the example com.myschool.kindie: somebody else may already own it.) You will type the same ID in step 4. Register the app. Firebase may ask you to skip or accept some extra pages: click Next, then Continue to console; you can keep Google Analytics off.
- Download
GoogleService-Info.plist and copy it into the project's root folder (next to package.json).

ls shows GoogleService-Info.plist and package.json side by side in the project folder.
Important: The Bundle ID in Firebase and in your app must be identical. It must be unique in the whole App Store and cannot be changed after you publish — choose it once.
🔧 4. Tell the app who you are and where your server is
- In Terminal, in the project folder, create your settings file and open it (
.env.local is a hidden file, so use this command instead of Finder). If you ran setup-environment.sh, .env.local may already exist; cp -n leaves it unchanged.
cp -n .env.example .env.local && open -e .env.local

The command that creates .env.local and opens it in TextEdit.
- In TextEdit, change the lines to the values below and save with Cmd+S. If TextEdit is in rich text mode, choose Format → Make Plain Text first.
# Your server — see the table below. "localhost" does NOT work on a real iPhone.
EXPO_PUBLIC_API_URL=http://192.168.1.20:1337
EXPO_PUBLIC_FILE_URL=http://192.168.1.20:1337
# Your own app identity
KINDIE_APP_NAME=My School
KINDIE_IOS_BUNDLE_ID=edu.myschool.kindie
# Optional, but recommended once you know it (Apple Developer account → Membership):
# KINDIE_APPLE_TEAM_ID=ABCDE12345
# FREE Apple ID? Remove the "#" from the next line (Apple does not allow push notifications with a free Apple ID, so Xcode refuses to sign the app without it):
# KINDIE_IOS_PUSH=off

An edited .env.local: the app name and the iOS bundle id are your own values. (Use your Mac's address as the server on a real iPhone.)
Which server address?
| Where your server runs | Value |
| On this Mac | http://<this Mac's IP>:1337. Find the IP in System Settings → Wi-Fi → Details (for example 192.168.1.20). Never localhost — to the iPhone, localhost is the iPhone itself. Your Kindie server must accept connections from other devices on the network, and the Mac's firewall must allow it |
| On the internet | https://kindie.myschool.com (your real address) |
A release app (the one parents install) accepts https:// addresses only. http:// to an address on your own network works for testing.
▶️ 5. Run the app on your iPhone
Note: Steps 2 to 6 below use the Xcode window and the iPhone screen, which are not shown in screenshots. To see the app without an iPhone, use the
iOS Simulator (step 8): it comes with screenshots.
- Make sure the iPhone is plugged in and unlocked, then run this in the project folder (or
bash run-ios.sh):
./run-ios.sh
The first time, it creates the ios/ folder, installs the iOS libraries (CocoaPods; takes a few minutes), and opens Xcode.
- In Xcode, in the left panel click the blue Kindie project, then the target Kindie, then the tab Signing & Capabilities:
- Tick Automatically manage signing.
- Team: choose your Apple ID (it appears with (Personal Team) if it is free). If it is not listed: Xcode → Settings → Accounts → + and sign in.
- The Bundle Identifier shown must be the one you put in
.env.local.
- Free Apple ID only: you turned push off in step 4 (
KINDIE_IOS_PUSH=off), so Xcode should not mention Push Notifications. If it does, the ios/ folder was created before you set that line: delete ios/ and run ./run-ios.sh again.
- At the top of Xcode, choose your iPhone as the destination (next to the Kindie name), then press the Play ▶ button (Cmd+R). The first build takes 10–30 minutes.
- Start the code server — in a second Terminal window, in the project folder. Keep it running. The test app loads its code from it. If the iPhone asks to find devices on your local network, tap Allow. On the app's first screen (a list of servers), tap the entry that shows your Mac's address.
yarn start --dev-client
Important: This test app only works while the code server is running (Mac on, Terminal window open). It is not the app your parents will install: that one comes from TestFlight or the App Store (docs/publishing.md).
- If this is your first run, Xcode may say Developer Mode is off on the iPhone: turn it on (Settings → Privacy & Security → Developer Mode), let the phone restart, then press Play again. Xcode may also ask for your Mac password to sign: choose Always Allow.
- On the iPhone, the first time Apple blocks the app: Settings → General → VPN & Device Management → your Apple ID → Trust. Open Kindie again.
🔄 6. After you change settings
| You changed | Do this |
EXPO_PUBLIC_API_URL / EXPO_PUBLIC_FILE_URL | Stop the code server (Ctrl+C), run yarn start --dev-client --clear, reopen the app |
KINDIE_IOS_BUNDLE_ID, KINDIE_APPLE_TEAM_ID, KINDIE_IOS_PUSH, KINDIE_APP_NAME, the icon, or anything in app.config.ts | Delete the ios/ folder, delete the app from the iPhone, then run step 5 again |
GoogleService-Info.plist | Copy the new file into the project root, then delete ios/ and run step 5 again |
| Only JavaScript code | Nothing — the code server reloads it |
🩹 7. When something goes wrong
| Message / symptom | What to do |
Xcode.app not found | Install Xcode (step 1) and open it once |
pod: command not found, or Pods errors | Run bash setup-environment.sh again. Then cd ios && pod install |
| Node version errors | node -v must say v20.11.x |
"CocoaPods requires your terminal to be using UTF-8 encoding", or pod install fails with "Unicode Normalization not appropriate for ASCII-8BIT" | Your Terminal has no UTF-8 language setting (echo $LANG prints nothing). Add export LANG=en_US.UTF-8 to ~/.zshrc, open a new Terminal and run the command again. The first message is only a warning on some Macs; on others the build stops with the second one |
| Xcode: "No account for team" / "Signing requires a development team" | Step 5.2: choose your Apple ID as Team |
| Xcode: "Failed to register bundle identifier" | The ID is already taken by someone else. Choose another KINDIE_IOS_BUNDLE_ID, delete ios/, run ./run-ios.sh again |
| Xcode: "Personal development teams do not support the Push Notifications capability" | Free account: set KINDIE_IOS_PUSH=off in .env.local (step 4), delete the ios/ folder and run ./run-ios.sh again |
| iPhone: "Untrusted Developer" | Step 5.6: Settings → General → VPN & Device Management → Trust |
| iPhone: "Developer Mode required" | Step 2.2 or 5.5 |
| Xcode: "Your team has no devices from which to generate a provisioning profile" | Plug the iPhone in, unlock it, and wait for Xcode to list it as a destination (step 5.3). Then press Play again |
| The app opens to a list of servers / "No development servers found" | The code server is not running or the iPhone is not on the same Wi-Fi as the Mac. Start yarn start --dev-client and check Wi-Fi |
| Login fails / "Network error" | The server address is wrong, or unreachable from the phone — see the table in step 4. Open it in Safari on the iPhone to test |
| The app stops opening after a week | A free Apple ID's test app expires after 7 days. Run step 5 again (no data is lost) |
| The app shows only a blank or plain-coloured screen for several minutes the first time | Normal. The code server (yarn start --dev-client) builds the app's code the first time the iPhone asks for it, which can take 5–10 minutes. Watch the code-server Terminal: wait until it prints iOS Bundled …. Later starts take seconds |
Many lines about CHHapticPattern, hapticpatternlibrary.plist or CoreUI in the terminal (iOS Simulator) | Normal. The Simulator has no haptic engine; the app is not affected. Ignore them |
| The build is stuck on "Connecting…" in the terminal | The app is already installed. Press Ctrl+C, then open the app on the phone |
Missing GoogleService-Info.plist | Step 3: the file must be in the project root before ios/ is generated |
| The app opens then closes at once | In Xcode: Window → Devices and Simulators → your iPhone → Open Console, reopen the app, and send the output to support |
Still stuck? Email zinisoft.net@gmail.com with: your macOS and Xcode versions (sw_vers, xcodebuild -version), the output of node -v, and the last 30 lines of the failed build.
🧪 8. Appendix — try it in the iOS Simulator (no iPhone)
You can see the app without a phone: install an iOS Simulator runtime in Xcode → Settings → Components, then run:
yarn ios

yarn ios creates the ios/ folder and starts installing CocoaPods.
The first time this takes several minutes (CocoaPods, then the Xcode build). The build ends with Build Succeeded:

The Xcode build ends with Build Succeeded and 0 errors.
The app is installed and opened on the Simulator, and the code server bundles the app code. Wait for iOS Bundled. The lines about statusBarStyle or haptics are normal.

The app is installed and opened on the Simulator; the code server prints iOS Bundled.
The Kindie sign-in screen appears in the Simulator:

The Kindie sign-in screen on the iOS Simulator.
Use http://localhost:1337 as the server address — in the Simulator, localhost is your Mac. If the terminal stays on "Connecting…", press Ctrl+C and press i in the code-server terminal. If the simulator window stays black, run killall DeviceHub and try again. A Simulator is not a substitute for testing on a real iPhone: camera, push and performance differ.
🎬 Video: Install the iPhone app on macOS (iOS Simulator)
The whole installation from start to finish, without sound. English captions are on the screen (about 1.5 min).
📚 9. Reference
| Item | Value |
| Node.js / Yarn / CocoaPods | 20.11.x / 1.22.x / 1.16.x |
| Expo SDK / React Native | 53 / 0.79.6 |
| Minimum iOS | 15.1 |
| JavaScript engine | Hermes. The New Architecture is deliberately off — do not switch it on |
| Development server port | 8081 |
| Permissions used | Camera, photo library, microphone, notifications |
| Xcode workarounds | Plugins withFmtXcode27Fix, withPodsDeploymentTarget and withIOS27SceneSupport re-apply them every time ios/ is generated. Do not edit files inside ios/ by hand: your changes are lost when the folder is regenerated — change app.config.ts or a plugin instead |
Push notifications need the paid Apple Developer account; there is no step-by-step guide for iPhone yet. For the web app and the Android app, see Push notifications.
Publish Mobile App
Turn on notifications for your families, and publish Kindie App to Google Play and the App Store under your own name. Notifications and the web app come first — they need only Firebase and your server. The store apps are built with EAS (Expo's build service), so you do not need Android Studio for the final file, and the iPhone app is signed for you.
The web app (PWA) needs none of the store steps. Parents can already use it from a browser (see "Mobile App Setup"). Publishing a phone app is optional and has costs and review times of its own.
| What it costs | Amount | Notes |
| Google Play developer account | One-time $25 | The identity check can take a few days. |
| Apple Developer Program | $99 / year | Needed for TestFlight and the App Store. Review takes 1–3 days per version. |
| Expo account (EAS build service) | Free plan available | The free plan has a monthly build limit and a build queue — see expo.dev/pricing. |
| A web page with your privacy policy | Free | Both stores require its address. |
🔔 Publish notifications
Kindie sends a notification when something happens — a new album, attendance, a payment, a day-off request, a chat message — or when staff send one from the Web Admin's Notifications page. Your Kindie server hands the message to Firebase Cloud Messaging, which delivers it to each parent's and teacher's device. You need your own free Firebase project (about 30 minutes). The steps below set up the web app; the Android phone app is added to the same project afterwards.
Step 1: Create a Firebase project
- Go to the Firebase Console, sign in with a Google account that will own the project, and click Create a project. Google Analytics and Gemini are optional.
Step 2: Register the web app and get the settings
- On the project's home page click the Web icon
</>, enter a nickname (for example Kindie App), leave Firebase Hosting off, and click Register app. Firebase shows a block of settings — keep that page open.
- Click the gear ⚙ → Project settings → Cloud Messaging → Web configuration → Web Push certificates → Generate key pair. The long text it shows is your VAPID key.
- Still in Project settings → Cloud Messaging, check that Firebase Cloud Messaging API (V1) is Enabled. If it is not, use the ⋮ menu → Manage API in Google Cloud Console → Enable.
Step 3: Put the settings in the app
Open config.js (public/config.js in the source, dist/config.js on your host) and fill in the values of your own project, one at a time — do not paste Firebase's whole block:
firebase: {
apiKey: "…",
authDomain: "…",
projectId: "…",
storageBucket: "…",
messagingSenderId: "…",
appId: "…",
measurementId: "",
},
vapidKey: "…the VAPID key from Step 2…",
Save, and press Ctrl + F5 in the browser. No rebuild is needed. Leave the firebase block empty if you do not want notifications — the app then works normally, without them. The same file also sets the app's language, colours and logo: see App Customization.
Step 4: Give your Kindie server the key
- In Firebase: ⚙ → Project settings → Service accounts → Generate new private key. This downloads a
.json file — keep it private.
- Install it on the server, never in the app, as described in "Application Configuration" → Push notifications (Firebase). The server must use the key of the same Firebase project as Step 2.
- Check it:
sudo docker compose -f docker-compose.prod.yml exec kindie node scripts/check-firebase-credential.js (Manual Install: node scripts/check-firebase-credential.js).
Step 5: Test it
- Open the app at its
https:// address, sign in as a parent or teacher, and click Allow when the browser asks about notifications.
- From the Web Admin, send a chat message to that user. Try it with the app in front, and again with the tab in the background.
- What to expect: with the app open you see an in-app banner and hear a sound; with the app minimized, a banner on the lock screen; with the app fully closed, Android still delivers it (see the note on phone makers below).
Step 6 (optional): Restrict the browser key
The apiKey in the web settings is not a secret — Google designed it to be public. Still, limit what it can be used for, but only after Step 5 shows that notifications work: a mistake here silently stops them.
- Open Google Cloud Console → APIs & Services → Credentials, choose Browser key (auto created by Firebase), set Application restrictions to Websites, and add the address parents open — the Kindie App, not your admin site — followed by
/*, for example https://app.myschool.com/*. Add http://localhost:8080/* too if you also test on your own computer.
- Do not restrict the APIs: Firebase Installations API and FCM Registration API must stay allowed. Changes can take a few minutes to apply.
Notifications on the Android phone app
The Android app uses the same Firebase project and the same server key as the web app. The app asks the phone's Google services for a registration token and sends it to your Kindie server, which sends notifications to it through Firebase. The VAPID key, config.js and the web key restriction are for the web app only.
- In the Firebase Console open the same project → ⚙ Project settings → Your apps → Add app → Android.
- For Android package name type exactly your
KINDIE_ANDROID_PACKAGE (see "Make the app yours" below). Leave the SHA-1 empty. Register the app and download google-services.json.
- Put the file in the project folder, next to
package.json, then build and install the app. The file is part of the build, so after replacing it delete the android/ folder and build again.
- Check that all your Firebase files agree:
yarn check-firebase-project-config prints a ✔, or tells you which file points at another project.
- Open the app, sign in, and tap Allow when it asks about notifications (Android 13 or newer asks; older versions do not). Test by sending a chat message from the Web Admin — it should appear in the phone's notification area within a few seconds, with the app open, in the background and swiped away.
Phones from Xiaomi, Oppo, Realme and Vivo close apps in the background: switch off battery saving for the app and allow Autostart. On an Android emulator, use an image that says Google APIs — others have no Google services.
Notifications on iPhone
- The web app on an iPhone or iPad: it runs in Safari, and Safari only allows notifications after the user adds the app to the Home Screen (Share → Add to Home Screen) and opens it from there. This needs iOS 16.4 or newer and an
https:// address.
- The iPhone store app: notifications need the paid Apple Developer Program (a team account). A free Apple ID cannot use them; if Xcode refuses to sign the app with "Personal development teams do not support the Push Notifications capability", put
KINDIE_IOS_PUSH=off in .env.local, delete the ios/ folder and build again — and leave that line out when you build with your paid account. For help setting up notifications on the iPhone store app, contact support.
If notifications do not arrive
| Problem | Cause and fix |
| Never asked for permission | The site is not on HTTPS, or the browser does not support push. Use HTTPS; on iPhone open the app from the Home Screen. |
| Permission was blocked | The user clicked "Block" earlier. Browser address bar → site settings → Notifications → Allow, then reload. |
| Allowed, but none ever arrives (web) | Wrong VAPID key, or the web key was restricted too much. Copy the VAPID key again; undo the restrictions of Step 6. |
| Works while the app is in front, not in the background | A file of the app is missing on the server. In the browser open the app's address followed by /firebase-messaging-sw.js, then by /config.js — both must show text, not an error. |
| Kindie sends, but the device shows nothing | Kindie's key file comes from another Firebase project. Create the key file in the same project as the app (Step 4). |
| Old behaviour after installing a new version of the app | The browser still runs the old version. Close every tab of the app (on a phone: close the app completely) and open it again. |
| Clicking a notification opens the home page, not the chat | Expected when the app was fully closed: it opens at the home page. Direct navigation works only while the app is already open. |
| Android app: allowed, but nothing arrives | google-services.json is from another Firebase project, or its package name differs from KINDIE_ANDROID_PACKAGE. Run yarn check-firebase-project-config, download the file again for the right package, delete android/ and rebuild. |
| Android app: the phone never asks about notifications | Phone Settings → Apps → your app → Notifications → Allow. |
🧰 Make the app yours (Android and iPhone)
Try the app on your own phone first: Android App and iPhone App take you from an empty computer to the app running on your phone. This page then turns that app into your school's app for the stores.
Everything that identifies the app is read from the file .env.local in the app source — copy .env.example to .env.local and edit it. The package ships with placeholders; publishing with them would put the app under a name that is not yours.
| Setting | Name in .env.local | Rule |
| App name | KINDIE_APP_NAME | Shown under the icon; 30 characters or fewer is safest. |
| iPhone Bundle ID | KINDIE_IOS_BUNDLE_ID | For example edu.myschool.kindie. Unique in the whole App Store. Cannot be changed after publishing. |
| Android package | KINDIE_ANDROID_PACKAGE | For example edu.myschool.kindie. Unique in the whole Google Play. Cannot be changed after publishing. |
| Apple Team ID | KINDIE_APPLE_TEAM_ID | 10 characters, from your Apple Developer account → Membership. |
| Server address | EXPO_PUBLIC_API_URL, EXPO_PUBLIC_FILE_URL | https:// only — the release app blocks plain http://. Both are your Kindie server's address. |
| EAS project ID | KINDIE_EAS_PROJECT_ID | Printed by eas init (see Step 3 below). |
Also edit version in app.config.ts (for example 5.0.0) and raise it for every update you publish; EAS raises the build number by itself. Google Play and the App Store both reject an upload whose version or build number is not higher than the previous one.
Pictures to replace
The pictures shipped with the package are small samples — a 192-pixel icon looks blurry on a phone and is rejected by the stores. Replace them and keep the file names:
| File | What it is | Size to use |
assets/kindie_icon.png | The app icon (iPhone, store listings, web) | 1024 × 1024 px, PNG, no transparency (App Store rule; Google Play shrinks it to 512). |
assets/ic_launcher_adaptive_fore.png | Android launcher icon: your logo only, on a transparent background | 1024 × 1024 px, PNG, transparent; keep the logo inside the central two thirds (Android crops the edges). |
assets/ic_launcher_adaptive_back.png | The picture shown on the splash screen while the app starts | At least 1024 × 1024 px, PNG. |
After changing any of these, delete the generated android/ and ios/ folders so they are created again with your values.
Keep .env.local and both Firebase files (google-services.json, GoogleService-Info.plist) safe and private. They are deliberately not part of the package or of git. Make a copy in a password manager or private storage — if you lose them you cannot reproduce your build.
☁️ Connect to EAS (once, for Android and iPhone)
Step 1: Create an Expo account and a git repository
- Create a free account at expo.dev.
- EAS needs the project to be a git repository, and the package you unzipped is not one yet. In a terminal in the project folder, once (install Git from git-scm.com first if the command is not found):
git init
git add -A
git commit -m "My Kindie app"
Your private files stay out of the commit automatically: git ignores .env.local and the Firebase files.
Step 2: Install the EAS tool and sign in
npm install -g eas-cli
eas login
eas init
On a Mac, if the first command says "permission denied", put sudo in front. eas init creates your EAS project and prints a project ID (letters and digits with dashes). Put it in .env.local. If eas init says it cannot save the ID into the configuration, that is fine — just copy the ID it printed:
KINDIE_EAS_PROJECT_ID=<the ID it printed>
Step 3: Give EAS your settings
EAS builds in the cloud and does not upload .env.local or the Firebase files. Create the same values as EAS variables, one command each. Run the whole set twice: once with --environment production (store builds) and once with --environment preview (the test APK) — a build only sees the variables of its own environment.
For every build:
eas env:create --environment production --name EXPO_PUBLIC_API_URL --value https://kindie.myschool.com --visibility plaintext
eas env:create --environment production --name EXPO_PUBLIC_FILE_URL --value https://kindie.myschool.com --visibility plaintext
eas env:create --environment production --name KINDIE_APP_NAME --value "My School" --visibility plaintext
eas env:create --environment production --name KINDIE_EAS_PROJECT_ID --value <your project ID> --visibility plaintext
Android only:
eas env:create --environment production --name KINDIE_ANDROID_PACKAGE --value edu.myschool.kindie --visibility plaintext
eas env:create --environment production --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json --visibility secret
iPhone only:
eas env:create --environment production --name KINDIE_IOS_BUNDLE_ID --value edu.myschool.kindie --visibility plaintext
eas env:create --environment production --name KINDIE_APPLE_TEAM_ID --value <your 10-character Team ID> --visibility plaintext
eas env:create --environment production --name GOOGLE_SERVICE_INFO_PLIST --type file --value ./GoogleService-Info.plist --visibility secret
Check with eas env:list --environment production (then preview): you should see every name you created. The command syntax comes from EAS and may change; if a command is rejected, run eas env:create --help.
🤖 Publish the Android app
Step 1: Add the app to Firebase
- Do the steps of "Notifications on the Android phone app" above: add an Android app to your Firebase project with the package name from
KINDIE_ANDROID_PACKAGE, and put the downloaded google-services.json next to package.json. The build stops without it.
Step 2: Try it first — a test file you can send directly (APK)
eas build --platform android --profile preview
- EAS asks "Generate a new Android Keystore?" — answer Yes. A keystore is the digital signature that proves future updates come from you.
- When the build finishes, EAS gives you a download link and a QR code. Open the link on a real Android phone, allow install from this source, install the app and sign in. This is the right first build to check that your settings are correct, before you go further.
Step 3: Build the release for Google Play (AAB)
eas build --platform android --profile production
Download the .aab file from the link EAS shows — this is the file Google Play accepts.
Step 4: Publish in the Google Play Console
Before you start, know what blocks most first-time publishers in the Google Play Console:
- Developer account: a personal account created recently must run a closed test with about 12 testers for 14 days before it can use Production. An organisation account (the school's name) needs a D-U-N-S number. Google's rules change — read what the Console shows you.
- App access: the app needs a login, so give Google's reviewers a demo parent and teacher login (App content → App access).
- Listing pictures: at least 2 phone screenshots, a 512 × 512 icon and a 1024 × 500 feature graphic.
- App content forms: the privacy policy address, Data safety, content rating and target audience. Kindie App uses the camera, microphone and notifications, and users can delete their own account from the app's Settings screen — say so where Google asks.
Then: create the app (name, language, free) using the same package name as KINDIE_ANDROID_PACKAGE → Testing → Internal testing (or Production, once allowed) → Create new release → upload the .aab → add the store listing → send for review.
Back up your Android signing key. EAS creates and stores it for you. Run eas credentials, choose Android, your profile, and the option to download the keystore (menu wording may differ), and keep the file somewhere safe. Google Play normally uses Play App Signing for new apps: then the key from EAS is only an upload key that Google can reset if you lose it — keep the copy anyway.
🍎 Publish the iOS app
Everything here about iPhone requires the paid Apple Developer Program. You do not need a Mac for the cloud build.
Step 1: Join the Apple Developer Program
Enrol at developer.apple.com/programs/enroll. Choose carefully:
- Individual — quick (usually a day or two); the App Store shows your personal name as the seller.
- Organization — the school's name is shown, but Apple requires a D-U-N-S number and verification, which can take days or weeks. If you want "under the school's name", start this early.
After joining, accept Apple's agreements in App Store Connect and note your Team ID (developer.apple.com → Membership details): it goes into KINDIE_APPLE_TEAM_ID.
Step 2: Add the app to Firebase
- In the Firebase Console open your project → Add app → the iOS icon, enter your
KINDIE_IOS_BUNDLE_ID, register, and download GoogleService-Info.plist. Put it in the project folder next to package.json. The iPhone build needs it.
Step 3: Create the app in App Store Connect
- In App Store Connect → Apps → + → New App: choose your Bundle ID (the same as
KINDIE_IOS_BUNDLE_ID; register it first under Certificates, Identifiers & Profiles → Identifiers if it is not listed). It also asks for a SKU: any private reference you invent (for example kindie-myschool-001); parents never see it.
Step 4: Build and send it to Apple
eas build --platform ios --profile production
eas submit --platform ios
- The build asks you to sign in with your Apple ID once, then EAS creates the certificate and provisioning profile for you.
eas submit asks which app and which build: choose the app you created and the latest build.
Step 5: Test with TestFlight, then submit for review
- TestFlight: your build appears after Apple's automatic checks (a few minutes to an hour). People install Apple's free TestFlight app first, then your app from it. TestFlight builds expire after 90 days. Internal testers (up to 100) are people you add under Users and Access — no review. External testers (parents) are added to a test group by email or public link; the first build needs Test Information and a short Beta App Review.
- App Store: fill in the description, screenshots (Apple lists the required sizes in App Store Connect), the privacy policy address, the App Privacy answers (the app uses the camera, microphone, photos and notifications; users can delete their own account in the app's Settings screen), the age rating, and a demo account (a parent and a teacher login) so Apple's reviewers can sign in. Then submit for review.
✅ Before you submit, and when updating
- App name, Bundle ID and package are yours — no
com.yourschool.kindie, no com.kindie.
- The server address is
https:// and opens in a browser.
- Icon and splash pictures are your school's.
- The Firebase files come from your Firebase project and match the Bundle ID / package.
- You installed the release build on a real phone (Android: the test APK; iPhone: through TestFlight) and signed in.
- The privacy policy address works, and a teacher and a parent demo account exist for the store reviewers.
- Signing keys and
.env.local are backed up.
When you install a new version of the app package
- Keep your
.env.local, google-services.json and GoogleService-Info.plist — copy them into the new folder, and compare .env.local with the new .env.example, because new settings may have been added.
- Delete
android/ and ios/ so they are generated again, raise version in app.config.ts, then build and submit as above.
Upgrading from Kindie v4
V5 is a new generation (React SPA + hardened API). There is no automatic data migration — and for a kindergarten you rarely need one: at the start of a school year, classes, fees and menus are set up fresh anyway.
🔄 Your options
- Recommended path: install V5 fresh, then re-import students, parents and staff via the Excel templates (health columns included). Keep your v4 instance read-only for one school year for historical lookups.
- Staying on v4: fine — v4 is not part of this package. v4 receives critical fixes only until 2026-12-31.
- Need help moving? A paid migration-assist service is available — contact the Kindie team.
Contact & Support
How to reach us, and where to find answers to common problems.
Hit a problem? Open the Troubleshooting tab at the top and choose Installation & Setup.
💬 Support
- Email: zinisoft.net@gmail.com · WhatsApp: +84 983 870 342
- Support hours: working days 8:00–17:00 (GMT+7), response within 1 working day
- Item support follows the standard Envato item support policy (6 months included, extendable at checkout). Free lifetime updates: enable update notifications at codecanyon.net/downloads