It is 3am. The baby is finally down, and you cannot remember which side the last feed was on, how much of the bottle went, or when the nap before it ended. That question is what baybay answers, and nearly all it does.
Open source feed and sleep tracking for the first year, built by a parent who got tired of apps that wanted an account, a subscription and a copy of the baby's data before they would show a bottle timer. Log a feed in two taps. Start a nap with one. See the pattern on a dashboard that fits on a phone at 3am. That is the whole product.
If someone runs it for you, all you ever touch is a web page. If you can paste five commands into a terminal, you can be that someone — Getting started is the whole install.
One screen does the daily work. Two big buttons log a feed or a sleep; an ongoing nap sits at the top with its running time until it ends. Under those, Recent lists the last couple of days, where the pencil edits an entry and the × removes it — a 3am mis-tap is a ten-second fix.
- Feed opens a sheet with your usual amounts one tap away — the presets are yours to change in Settings — and room for a note when something is worth remembering.
- Sleep is a stopwatch for naps: start it, come back whenever the baby is up. At bedtime, Night logs the whole stretch.
- Dashboard answers what parents actually ask — how long since the last feed, when they last slept, what a typical day looks like — then charts the patterns over recent weeks and lays out the last seven days in detail.
- Settings holds the household: children, the shareable code, bottle presets, the daily rhythm, signed-in devices, export, and the button that deletes everything for good.
Open your household's address on a phone and Add to Home Screen installs it like an app, icon and all. Lost the address? The front page emails it back to you.
Everything one family logs lives in exactly one SQLite file:
storage/households/quiet-heron-4821.sqlite
Nothing about that family lives anywhere else. This is not a policy, it is the schema — which makes two promises cheap to keep and easy to verify:
- Export hands you that file, as a consistent snapshot that opens in any SQLite browser, with CSV and JSON if you would rather have those.
- Delete unlinks it. There are no rows scattered across a shared table to chase, and no soft-delete flag quietly retaining what you asked to remove.
Run it yourself and even that file never leaves your machine.
- PHP 8.2 or newer, with
pdo_sqliteandopenssl - A web server pointed at
public/— or none at all; PHP's built-in server is enough to try it
No Composer, no npm and no build step. Two third-party files are checked in rather than fetched:
public/assets/lib/chart.js— Chart.js 4.5.0, MIT, which draws the dashboard charts.public/assets/fonts/JetBrainsMono-VariableFont_wght.ttf— JetBrains Mono, SIL Open Font License 1.1, whose terms are inOFL.txtbeside it.
Nothing else is vendored, and nothing is loaded from a CDN — the Content-Security-Policy would refuse it.
git clone https://github.com/webdevday/baybay.git
cd baybay
cp .env.example .envOpen .env and set the address the app will be served from. To try it on
your own machine:
DOMAIN_APP=localhost:8000
URL_SCHEME=http
Then:
php bin/baybay key:generate
php bin/baybay migrate
php bin/baybay household:create --child="Wren"That prints a URL, a household code and the path of the new data file. Serve it and open the URL:
php -S localhost:8000 -t public bin/dev-router.phpThe router script only matters to PHP's built-in server — it serves real
files and hands everything else to the app, which Apache and nginx do
themselves. For Apache, point a vhost at public/; the .htaccess files
shipped here do the rest. For nginx, start from deploy/nginx.example.conf.
A brand new household is empty, and an empty dashboard does not show much. To see what it looks like in use before trusting it with your own nights:
php bin/baybay household:seed <slug>With no mail server configured, magic links are written to
storage/logs/mail.log — follow them straight out of the log. That is the
MAIL_DRIVER=log default, and the right setting until you deploy somewhere
that can actually send mail.
Each household has its own permanent URL. Two ways in:
- Magic link — the owner enters their email and clicks a link. No password to forget while holding a baby.
- Household code — a short phrase like
moss-lark-42that a partner, grandparent or sitter types once. Rotate it from Settings and every session that joined with the old code is signed out.
The URL is the household's address, not its key. Both routes still require a session, so a link forwarded to the wrong person does not hand over the data.
Optional, and off until somebody asks for it. Set a schedule and the log screen gains one line — what is next and how long until it — that opens the right sheet when tapped, with Night already picked if the next thing is bedtime. Items tick themselves off as matching feeds and sleeps are logged; nothing is flagged as missed, because a skipped nap is a normal Tuesday.
Six age-based templates are built in, from a newborn's three-hourly day to a single midday nap. Applying one asks for the hour your baby actually wakes and moves the whole day to match, so a routine arrives in one tap rather than fourteen. Templates are copied in when chosen, so improving one in a later release never rewrites a schedule a parent has already adjusted. From there it is yours: rows are added, retimed, renamed and deleted in Settings, and a household can keep more than one rhythm and switch between them.
Administration happens through one script, run from the install directory.
php bin/baybay help prints the same list with more detail.
Setup
php bin/baybay key:generate Create APP_KEY and write it into .env
php bin/baybay migrate Apply pending migrations to the control
database and every household
php bin/baybay migrate:status Show what each database has applied
key:generate refuses to replace a key that is already set, because doing so
signs every household out and makes every household code unreadable. Clearing
the line by hand is the deliberate step it asks for first.
Households
php bin/baybay household:create Create one, with its own data file
--name="The Smiths" household name
--child="Wren" first child's name
--tz=Europe/Lisbon IANA timezone
php bin/baybay household:list List every household
php bin/baybay household:code <slug> Show the shareable code
php bin/baybay household:seed <slug> Fill it with sample feeds and naps
php bin/baybay household:purge <slug> Delete a household's data, for good
Maintenance
php bin/baybay prune Remove expired demos, sessions and tokens —
the one cron should run
php bin/baybay prune <slug> Remove one demo, by name
php bin/baybay prune --all Remove every demo, expired or not
php bin/baybay test Run the test suite
The tests build a disposable install under the system temp directory, so no run of the suite can touch a real household.
Everything is in .env, and it is read on every request — a changed value
takes effect on the next page load, with nothing to rebuild or restart.
.env.example documents every key at length and is worth reading in full
before changing one; this is the short version.
| Key | Default | What it does |
|---|---|---|
APP_ENV |
local |
local or production. Production tightens errors, cookies and analytics. |
APP_DEBUG |
true |
Verbose errors in the browser. Must be false in production. |
APP_KEY |
(empty) | Signs sessions, magic links and household codes. Set once by key:generate, then guard it — see Backups. |
APP_TIMEZONE |
America/Denver |
Only the default offered at signup; each household keeps its own. |
DOMAIN_APP |
baybay.local |
Where households live, no scheme. This becomes part of every household's permanent URL, so pick it before inviting anyone. |
URL_SCHEME |
https |
https in production, http on your own machine. |
BASE_PATH |
(empty) | Subdirectory prefix, only if the app is not at the web root. |
DOMAIN_APP_ALIASES |
(empty) | Extra hostnames to answer on in development — a LAN address, so a phone on the same Wi-Fi can open it. Empty in production. |
DOMAIN_MARKETING |
(unset) | Only if a separate site fronts this install. Left unset it equals DOMAIN_APP and every cross-site affordance hides itself — the right answer for self-hosting. |
CONTACT_EMAIL |
(empty) | Where a parent can reach whoever runs the install. Blank hides the affordances. |
ANALYTICS_APP_ID |
(empty) | Blank means no analytics and no third-party request at all — what a fork should keep. Every page it would measure is a named child's feed and sleep record; .env.example spells out what setting it means. |
SESSION_LIFETIME_DAYS |
90 |
How long a sign-in lasts. Deliberately long; refreshed on each visit. |
MAGIC_LINK_TTL_MINUTES |
30 |
How long an emailed link stays valid. |
MAIL_DRIVER |
log |
log writes mail to storage/logs/mail.log, mail uses the server's sendmail, smtp talks to a relay using the MAIL_SMTP_* keys. |
MAIL_FROM_ADDRESS / MAIL_FROM_NAME |
— | The sender on magic links and notices. Delivery depends on the domain's SPF, DKIM and DMARC records, not on baybay. |
Each family's data is one file, so a lost file is a lost account with nothing
to reconstruct it from. Do not copy those files with cp while the site is
serving — a copy taken mid-write can be torn, and the -wal sibling beside
each database holds committed data that has not been folded back in yet.
bin/backup.shTakes a consistent snapshot of the control database and every household,
writes storage/backups/YYYY-MM-DD-HHMM.tar.gz, and deletes archives older
than 30 days — set BAYBAY_BACKUP_KEEP_DAYS to change that. It resolves its
own install root, so it needs no editing and works from cron:
41 3 * * * /path/to/baybay/bin/backup.sh >> /path/to/baybay/storage/logs/backup.log 2>&1Restoring one family is copying their .sqlite back into
storage/households/. There is nothing else to it — no rows scattered across
shared tables.
Back up .env too, privately and separately from the data. It holds
APP_KEY, and restoring households without their key leaves every household
code unreadable and signs everyone out.
git pull
php bin/baybay migrateEvery database records which migrations it has applied, and any that are behind catch up on the next request. There is nothing to edit per household.
public/ Web root — the only directory a server should expose
app/ Application code (PSR-4 under Baybay\)
Http/ Routing, requests, responses
Controllers/ Request handlers
Services/ Recent activity, daily totals, schedules, preferences, charts
Domain/ Households, sessions, sign-in, sample data
Database/ Connections and migrations
Support/ Config, clock, crypto, mail, strings
resources/views/ Page templates and layouts
database/ Migrations, and the control database once created
storage/ Household data files, backups and logs
deploy/ Example nginx config
tests/ The suite behind `php bin/baybay test`
bin/baybay Command line tool
bin/backup.sh Consistent snapshot of every database
bin/dev-router.php Router for PHP's built-in server, development only
Two databases, deliberately. database/control.sqlite knows which households
exist and who may sign in. It holds no logged baby data of any kind. Everything a parent actually records lives in
that household's own file under storage/.
This repository is the application and nothing else. There is no payment code to leave switched off, no landing page, and no dependency on a payment provider — which is why the install above has no Composer step. Nothing is gated, metered or waiting to be unlocked: every feature baybay has is in this repository, running on your own server.
That goes for the schema too. There is no subscription table sitting empty,
no invoice ledger and no customer id anywhere in database/migrations/ — the
control database knows which households exist and who may sign in, and stops
there. A deployment that does take money registers its own migrations at boot
and keeps them in its own repository, so nothing about billing has to be
carried by, or explained to, the people running this.
MIT. See LICENSE.