snagtime: a scheduling app whose local demo calls nobody
Free, self-hostable scheduling app with booking links, Google Calendar sync, SMTP notifications, and Stripe test payments.
At a glance
- What is it?
- A self-hostable booking system with public booking links, calendar free/busy checks, SMTP notifications and Stripe test-mode payments. The interesting engineering is the offline path: SQLite, a local calendar adapter, a local email inbox and stub payments, so nothing leaves your machine until you configure keys you own.
- Who is it for?
- snagtime suits someone who wants to own their scheduling stack and can accept running it: a Node web service, a worker, a hardened PostgreSQL and your own domain. The local demo is the cheap way in, since it needs no keys at all and the setup command prints your own login once.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 34 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One command runs the app without a single credential
The local path is designed so that nothing external is involved. The requirements are Node.js 20.9 or newer with 24 as the verified runtime, npm and Git, then three steps:
git clone https://github.com/nateherkai/snagtime.git
cd snagtimenpm run setupnpm run demo:freeAfter setup you open http://localhost:3000 and use the login it printed. The setup command creates an ignored .env.local, generates independent cryptographic secrets, and prints the local demo login once. If you would rather choose the credentials yourself, the same command takes them:
npm run setup -- --email [email protected] --password "YourStrong!Password7"What makes this cheap is the substitution table rather than a mock flag. The credential-free local mode uses SQLite, a local calendar adapter, a local email inbox and stub payments, and the readme states plainly that it does not call Google or Stripe. An email inbox you can read locally is more useful than a fake one, because you can see what would have been sent.
There is also an assisted path. The documentation includes an AI setup guide with a student prompt to paste into Codex or Claude Code along with the repository URL, and it is written to keep the assistant from pulling you into infrastructure work before the app runs.
Two database schemas, and only one of them is for production
The prisma directory holds a SQLite schema, a PostgreSQL schema and migrations, which is the first sign that the local demo and the production deployment are deliberately different systems sharing one codebase.
The local database is a single file. The .env.example sets DATABASE_URL to a file path relative to prisma/schema.prisma, and the role variable is app. Separate variables exist for the worker and monitor connections.
Production is described as intentionally strict: PostgreSQL 18, forced row-level security, separate app and worker credentials, verified database TLS and authenticated proxy ingress. The .env.example carries four operator-only database passwords and a comment that says never mount these in the web or worker containers, which is the sort of instruction that only appears where someone has already made the mistake.
So the cost of moving from demo to production is not configuration, it is an architecture change with a hardening model attached. That is the honest framing for an application that stores booking links people can share publicly.
One detail worth copying: db:reset destroys the local SQLite database, and the readme says to use it only when you intentionally want a clean demo. Naming a destructive script and its blast radius in the documentation is cheaper than the support ticket.
The integration table says no four times
External integrations are laid out in a table with a column for whether they are required for the local demo. Every row answers no.
Database: nothing for SQLite, PostgreSQL for production. Public hosting: an HTTPS domain plus a long-running Node web service and worker. Google Calendar: an OAuth client ID and client secret. Transactional email: an SMTP host, user, password and verified sender domain. Stripe payments: a Stripe test secret, a publishable key and a webhook secret.
The principle is stated once above the table: copying the repository gives you all of the software, while external integrations still belong to you and must be configured with your own accounts. That sentence is the difference between a demo that works out of the box and a demo that works because the author's keys are in the repository.
The .env.example keeps the two apart as well. Calendar is opt-in with the default provider set to local and a comment that the default makes no network calls, email has its own provider variable also defaulting to local, and Google fields for client ID, secret, refresh token and calendar ID sit empty. Environment-managed Google credentials are marked local-demo-only and must be bound to one immutable workspace ID.
Exact callback URLs and verification steps are in a separate integration guide rather than in the readme.
Live-mode Stripe keys are refused on purpose
Payments exist in the feature list as Stripe Checkout in test mode, including webhook confirmation and refund handling. Event types carry optional test pricing, and the workflow covers booking confirmation, rescheduling, cancellation and recovery links.
The refusal is the part to notice. This release deliberately rejects Stripe live-mode keys, and the readme adds a warning against advertising it as a live payment processor without implementing and auditing live-mode support yourself.
That is a stronger position than simply omitting live mode. A repository that refuses live keys cannot be deployed by someone who wires in their own by accident, and the failure mode is a startup error rather than a charge to a real card. For anyone who does want to take money, the path is explicit: implement it, then audit it.
The same restraint shows up in the hosting section, where named platforms are ruled out rather than quietly left broken.
Two hosting options are ruled out by name
The deployment section opens by saying what the application is: dynamic, not a static website. It needs server-side Node.js execution, a persistent database, webhook endpoints and a continuously running background worker.
Then two exclusions. ChatGPT Sites is not a compatible production host for this repository. Vercel is not supported out of the box, because the current production design requires a long-running worker and PostgreSQL runtime roles.
Stating the Vercel exclusion with the reason attached is the useful part. It is not that the framework does not run there, it is that a continuously running worker and separate database roles do not fit the platform's execution model, and saying so saves someone an afternoon of building a deployment that fails at the webhook.
The shape that is recommended is a Linux VPS or a container platform that supports a web service, a worker service, persistent PostgreSQL, secrets and HTTPS. The deployment guide is the thing to read before choosing a host, which is also what the readme tells you to do.
The .env.example supports that shape with a note that the standard dev command keeps the local outbox poller alive, a five-second poll interval, and the instruction to keep the Node process long-lived so calendar and email jobs continue to drain.
Twenty-one CI scripts, and most of them are about Postgres
The scripts list is where this repository's priorities show. Alongside the ordinary setup, dev, build, test, typecheck and lint entries, there is a set of continuous integration checks whose names read like a security review.
They are ci:secret-scan, ci:audit-policy, ci:generated-drift, ci:backup-contract, and then eight checks that all begin ci:postgres: guards, runtime, rate-policies, health, authority, provision-negative, import-negative and rls. Row-level security appears both as a check name and as the production architecture's defining feature, which is consistent.
The negative tests are the interesting half. A provision-negative and an import-negative check assert that an operation fails when it should, which is the only way to know a guard is enforced rather than merely present.
There are two more commands outside that family worth knowing: setup:check validates the credential-free local configuration, and prod:check runs a production config check in runtime mode. The worker has its own build and start scripts, and dev:persistent has a matching stop.
The documented command set is small and clean: setup, setup:check, demo:free, dev, test, typecheck, lint, build and ci:secret-scan, plus db:generate, db:migrate and db:seed. So the gap between the readme and package.json here is the safety checks, not the basics.
The image refuses to build without an immutable BUILD_ID
The Dockerfile treats reproducibility as a build requirement rather than a convention.
The base image is node:24.15.0-bookworm-slim pinned by digest, so the same Dockerfile builds the same userspace later. Dependencies are installed with npm ci and scripts disabled. Then a build argument is validated before anything else runs: if BUILD_ID is missing or is not a run of 40 to 64 hex characters, the build throws. That check is inline in the Dockerfile, so it fires before the application is even compiled.
The stages are conventional and thorough. A deps stage copies the manifests, a builder stage generates both database clients and builds the worker and the web app, and a production-deps stage prunes dev dependencies, removes the Prisma packages and a few others, and then runs a runtime dependency check over what is left. The runtime stage copies the standalone build, the static assets, the worker bundle and two scripts, then drops to a numeric user 1000:1000 with port 3000 exposed and a container entrypoint.
Two things stand out. Pruning then verifying is the correct order for catching a dependency that only exists because a dev package was present. And USER 1000:1000 rather than a named user is the other half of the same idea: nothing in the image should be able to write files the application expects to own.
The status paragraph assigns the work to you
The project status section is short and unusually direct. The local SQLite experience and the integration test paths are designed for demos, development and personal experimentation. The production architecture is an advanced self-hosting path, not a one-click managed service. And you are responsible for infrastructure security, backups, provider configuration, deliverability, compliance and ongoing operations.
Six domains handed to the operator in one sentence, including deliverability, which is the one people forget until their confirmation emails stop arriving. The instruction to read SECURITY.md before exposing a deployment publicly is repeated at the end of that section rather than left in a footer.
The licence is MIT and the package version is 0.1.0, marked private, with npm workspaces over apps/*, a Next.js application and API routes in apps/web, and separate directories for infrastructure hardening, browser and end-to-end tests, and documentation.
There are no GitHub releases, and the last push is dated 28 August 2026. Contributions go through issues and pull requests with the detail in CONTRIBUTING.md.
What is free is answered precisely as well: the source under MIT, a local run costing nothing, and a public deployment still creating third-party costs for hosting, a domain, managed PostgreSQL, email volume and any connected provider.
Editorial conclusion
snagtime suits someone who wants to own their scheduling stack and can accept running it: a Node web service, a worker, a hardened PostgreSQL and your own domain. The local demo is the cheap way in, since it needs no keys at all and the setup command prints your own login once. Before you expose anything, read SECURITY.md and the deployment guide, because the project states plainly that infrastructure security, backups, provider configuration, deliverability, compliance and operations are yours. And if you plan to take payments, note that this release rejects Stripe live-mode keys on purpose.
Frequently asked questions
Do I need a Google or Stripe account to run snagtime locally?
No. The credential-free local mode uses SQLite, a local calendar adapter, a local email inbox and stub payments, and it makes no calls to Google or Stripe. You run it with npm run setup followed by npm run demo:free, then open http://localhost:3000 and use the login the setup command printed.
Can snagtime take real payments?
Not in this release. Stripe Checkout runs in test mode with webhook confirmation and refund handling, and live-mode keys are deliberately rejected. The readme warns against advertising it as a live payment processor without implementing and auditing live-mode support first.
Can I deploy snagtime to Vercel?
Not out of the box. The production design requires a long-running worker and PostgreSQL runtime roles, which is why Vercel is listed as unsupported, and ChatGPT Sites is also named as incompatible. A Linux VPS or a container platform with a web service, a worker, persistent PostgreSQL, secrets and HTTPS is the recommended shape.
What does the snagtime production setup require?
PostgreSQL 18 with forced row-level security, separate app and worker credentials, verified database TLS and authenticated proxy ingress. The .env.example carries operator-only database passwords that must never be mounted in the web or worker containers.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nateherkai-snagtime)