Attendance and leave for teams that live in Slack.
Your team already types "morning!" in #general. offon turns that into a real attendance record — clock-in, breaks, leave requests, approvals — without asking anyone to learn new software.
Self-hosted. MIT licensed. Deploys to Vercel in about 10 minutes.
Quick start · Using offon · Slack setup · Self-hosting · Contributing
You use Slack. You have no attendance system — just a spreadsheet someone updates, a channel where people post that they've arrived, or nothing at all. You don't want to buy an HR suite, and you'd rather not hand your team's working hours to a vendor.
offon is for that gap. It's a small app you run yourself, and the whole interface your team touches is Slack.
It is not a payroll system, an HRIS, or a multi-tenant SaaS. One deployment serves one organization, on purpose.
Someone starts their day by typing this in Slack:
/hi
They're clocked in, and the team channel says so. At lunch, /lunch — a meal runs for a fixed length your admin sets, an hour by default, so they come back automatically without a second command. Stepping out is /break, coming back is /back, and the day ends with /bye.
Everything else — leave requests, approvals, the calendar, corrections when someone forgets to clock out — lives in a web app that people sign into with a code DM'd to them on Slack. No passwords to manage, no accounts to provision.
docs/using-offon.md is written for your team — hand it to them once you're deployed.
- Attendance — Clock in/out, away-from-desk breaks, meals, and multiple sessions per day (crossing midnight is handled). Worked, break, and overtime minutes are computed for you.
- Meals without a second command — Starting a meal records a break that closes after the configured length, so the return happens by the clock. Clocking out and starting a break are blocked until it ends, and the "back now" notice is scheduled with Slack up front.
- Leave — Full-day and half-day (AM/PM) requests, business-day counting that skips weekends and the holidays you configure, balance tracking, and approve / reject / cancel flows.
- Correction requests — Forgot to clock out? Request an edit from the calendar; a manager approves and the totals recalculate. In-progress sessions can be corrected too.
- Meeting rooms — Weekly booking grid with conflict detection, attendee lists, a Slack DM to everyone when a meeting is booked, and a reminder 3 minutes before it starts.
- Approvals in one place — Pending leave and correction requests sit on a single admin page, with a badge in the nav so nothing rots.
- Calendar — Month view of personal attendance and leave, team leave, and per-member search.
- Excel export — Hand your accountant a spreadsheet instead of a screenshot.
- Passwordless login — A 6-digit code, DM'd on Slack, hashed with argon2.
- Scheduled nudges — Missing clock-in and clock-out reminders, and yearly leave rollover.
- English and Korean — Each person picks their language from the header; Slack messages follow
DEFAULT_LOCALE. - Any timezone — Set
NEXT_PUBLIC_TIMEZONEto your IANA zone. Day boundaries and totals follow it, daylight saving included. - Your own policy — Working hours, meeting-room hours and the meal length are admin settings, not constants. A standard day, what a half day is worth, and where morning becomes afternoon all derive from the first two, so they cannot drift apart. Changing the meal length applies to meals started from then on; the ones already recorded keep their own.
![]() |
![]() |
| Calendar — a month of your own attendance and leave | Meeting rooms — a week grid you book by dragging |
![]() |
![]() |
| Approvals — leave and correction requests in one queue | Members — the team and their leave balances |
You need Node.js (LTS), pnpm, Docker (for a local database), and a Slack workspace you can install an app into.
git clone https://github.com/takealook97/offon.git
cd offon
pnpm install
cp .env.example .env.local # then fill it in — see Configuration below
pnpm db:local:up # local Postgres in Docker
pnpm db:migrate:deploy # create the schema
pnpm db:seed # create the first admin
pnpm dev # http://localhost:3000Before pnpm db:seed, you need a Slack app for the login codes to go anywhere. docs/slack-app.md walks through it — it's a manifest you paste in, and it takes about five minutes.
Then sign in with the admin's email. A 6-digit code arrives in that person's Slack DM.
Prefer Docker? docker compose --profile app up --build brings up the app and a database together — see docs/self-hosting.md.
On Vercel you'll need a Postgres database — Supabase, Neon, Railway, RDS, or your own all work. See docs/self-hosting.md for the full walkthrough, including the connection-pooling setting that matters on serverless and how the scheduled jobs are wired.
Copy .env.example to .env.local and fill in these:
| Variable | Required | What it is |
|---|---|---|
DATABASE_URL |
yes | Postgres connection string |
SESSION_SECRET |
yes | Signs the session JWT. Rotating it logs everyone out. |
OTP_PEPPER |
yes | Extra secret mixed into login codes before hashing |
SLACK_BOT_TOKEN |
yes | Bot User OAuth Token (xoxb-…) |
SLACK_SIGNING_SECRET |
yes | Verifies slash commands really came from Slack |
SLACK_OFFON_CHANNEL |
yes | Channel ID for clock-event announcements |
NEXT_PUBLIC_TIMEZONE |
no | The IANA timezone your team works in (default Asia/Seoul) |
DEFAULT_LOCALE |
no | Language for Slack messages and reminders (ko or en, default ko) |
CRON_SECRET |
production | Authorizes the scheduled-job endpoints |
SEED_ADMIN_* |
seed only | Used once by pnpm db:seed to create the first admin |
Generate the two secrets with openssl rand -base64 32.
Worth knowing before you deploy — these are real, and PRs are welcome on all of them.
- Postgres only. The schema and migrations are Postgres-specific. Supabase, Neon, Railway, RDS all qualify; MySQL and SQLite do not.
- One organization per deployment. There's no tenant concept in the schema — run a second deployment for a second org.
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, proxy.ts) |
| Language | TypeScript, React 19 |
| Styling | Tailwind CSS v4, shadcn/ui |
| Database | PostgreSQL via Prisma 6 |
| Auth | JWT (jose) in an httpOnly cookie; Slack OTP hashed with argon2 |
| Validation | zod |
| Calendar | react-big-calendar, react-day-picker, date-fns |
| Slack | @slack/web-api |
| Hosting | Vercel (cron declared in vercel.ts) |
- Stateless sessions. Identity lives in a signed JWT in the
sessioncookie, verified inproxy.tsand again in route guards (requireSession/requireAdmin). There's no session store to run. - Ownership is enforced on the server. Employees can only read and edit their own attendance; corrections and cancellations are scoped to the owner, and approvals require an admin. The client is never trusted for this.
- Wall-clock times, stored as UTC instants. Day boundaries come from
Intl's timezone database rather than a fixed offset, so a day that loses or gains an hour to daylight saving is 23 or 25 hours long and the totals still add up. Formatting is independent of the runtime clock. - Soft deletes everywhere. Records carry
deletedAtrather than disappearing, because attendance is a record of what happened. - Policy is derived, not repeated. A standard day is the working window minus the meal; a half day is half of that; the morning/afternoon split is the start plus a half day. Writing those out as separate constants is how a settings screen and a month-end spreadsheet come to disagree without anyone noticing.
| Script | Purpose |
|---|---|
pnpm dev |
Start the dev server |
pnpm build |
prisma generate + production build |
pnpm test |
Run the test suite |
pnpm test:db |
Run the suite that needs a real Postgres |
pnpm test:e2e |
Drive a browser against a real build |
pnpm test:gaps |
List modules in src/lib no test imports |
pnpm lint |
ESLint |
pnpm db:seed |
Seed the initial admin member |
pnpm db:migrate:deploy |
Apply pending migrations |
pnpm db:studio |
Open Prisma Studio |
pnpm db:local:up / :down |
Start / stop local Postgres (Docker) |
src/
├── app/
│ ├── (app)/ # Authenticated app (dashboard, calendar, rooms, admin)
│ └── api/ # Route handlers (auth, attendance, leave, calendar, cron, slack)
├── components/ # Shared UI (shadcn/ui under components/ui)
└── lib/ # Prisma client, auth/session, time, Slack, domain logic
prisma/ # schema.prisma + migrations
docs/ # Slack app setup, self-hosting guide
Issues and pull requests are welcome — see CONTRIBUTING.md for how to get set up and what a good change looks like. The Known limitations above are all fair game.
Found a security problem? Please report it privately — see SECURITY.md.
MIT — see LICENSE.




