Skip to content

Latest commit

 

History

244 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

offon

offon

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.

CI License: MIT Next.js 16

Quick start · Using offon · Slack setup · Self-hosting · Contributing

offon dashboard


Who this is for

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.

How it works

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.

Features

  • 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_TIMEZONE to 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.

What it looks like

Calendar Meeting rooms
Calendar — a month of your own attendance and leave Meeting rooms — a week grid you book by dragging
Approvals Members
Approvals — leave and correction requests in one queue Members — the team and their leave balances

Quick start

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:3000

Before 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.

Deploying

Deploy with Vercel

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.

Configuration

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.

Known limitations

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.

Tech stack

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)

Design notes

  • Stateless sessions. Identity lives in a signed JWT in the session cookie, verified in proxy.ts and 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 deletedAt rather 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.

Scripts

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)

Project structure

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

Contributing

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.

License

MIT — see LICENSE.

Releases

Packages

Contributors

Languages