Open-source HR information system — people, hiring, leave, attendance, payroll, and an HR assistant that actually knows your policies.
Built for Thai labour practice. Designed to be self-hosted.
English · ภาษาไทย · Overview page
Try it now in your browser →
A shared demo company: sign in as an employee, a manager or HR in one
click — no sign-up, no password. It goes back to the start every hour, and may
take a minute to wake if nobody has used it lately. How it
is run.
The film, three and a half minutes — September's payroll calculated and approved by two different people, a clock-in on a site with no signal, leave filed from a phone and approved from the console, PDPA in practice. Every field is typed and every button pressed on camera, against the seeded demo company, by a script in this repository. Watch it on the overview page · MP4 · the Thai version
Real console, real second factor, real payroll run — recorded against the
company npm run db:seed builds, by a
script in this repository, on the English interface. Thai is the default and
one switch turns the whole console English (CW-016).
Higher-quality MP4 ·
the Thai take.
This month Cwork was built out for one kind of company in particular: a small
business with no IT department, staff who clock in on a fingerprint scanner,
wages paid in cash, and an outside accountant who files the withholding tax.
Everything below is in main and is currently in the free edition (Apache 2.0,
outside ee/). It is covered by unit and end-to-end tests in CI, and the scanner
import, the accountant's file and the NAS scripts also went through a QA pass
of 41 cases on synthetic data (plan and results).
No company runs Cwork in production yet.
The real console on the demo company, with a made-up scanner file: choose it, check the preview, import, then decide a day that waits for HR. Recorded by a script in this repository; every figure and name on this page is synthetic.
-
The fingerprint scanner's file, read as it is. Attendance → scanner days: upload the scanner's export, look at the preview, then import; nothing is written before that. A day with two times counts as in and out. A day with one time, or three or more, waits for HR, who sees every raw time and picks in and out or leaves the day uncounted: Cwork never settles an odd day by itself. Once a period has been paid its scanner days are locked, and a day forgotten before payday is corrected in the next period. Guide (Thai)
-
A cash payday, counted to the coin. From an approved run, print a cash-preparation sheet: for each person, how many of each note and coin down to 25 satang, and the total to withdraw from the bank. Whatever is left under 25 satang is shown, never rounded into anyone's pay. Payslips print one, two or four to an A4 sheet, with a wage-receipt sheet to sign. Guide (Thai)
-
A file for the outside accountant, and a check against what was filed. An approved or paid period exports with each person's tax ID and income type, for whoever prepares the ภ.ง.ด.1. Enter the tax and social security totals that were actually filed and Cwork shows its own figure beside each; one satang either way is shown as a difference. Upload your current per-person figures to run Cwork beside the way you pay today: every person and column that differs is marked, and nothing is changed. Cwork prepares and checks. It does not file anything, and it does not replace the accountant.
-
An installer for the office NAS.
deploy/nas/holds a read-only check, then install, backup, restore, update and rollback scripts, with HTTPS on one port. Rehearsed on a clean Docker host, where install, update, rollback and a byte-for-byte restore passed. Not yet tried on a real NAS: the Synology DSM screens, installing the certificate on Windows, a port scan from outside the office, and the layout with the app on a PC and the database on the NAS. Guide (Thai) -
Passports and work permits that do not lapse unnoticed. A list of the documents expiring within 90, 60 or 30 days, and a notice to whoever may edit the employee at each of those points and on expiry. The document number is never shown in the list or the notice. Guide (Thai)
-
Smaller things. Add a single employee from the console instead of a one-row import; set or correct a scanner number on the employee page; and a "report a problem" page that builds a text with diagnostics and no personal data, for you to copy and send yourself. Cwork sends nothing off your machine. Guide (Thai)
Six problems a Thai business pays for every month, and how Cwork answers each one — rarely with a single feature, usually with several working together. The overview page tells the same story with screenshots.
Per-seat pricing means every hire raises the bill, the contract renews every year, and your employee data lives on somebody else's servers.
- No licence fee, no per-seat fee. The free edition is Apache-2.0: twenty employees or two thousand cost the same — the server.
- One machine. Three Docker commands. PostgreSQL alone does the work of a queue, a lock service and a rate-limit store, so there is no Redis or Kafka to run.
- Your data, in your database. Back it up, move it, or leave; nothing locks you in.
- Room to grow. More API replicas are one setting, not another service.
A spreadsheet one person understands, progressive tax, social security and several overtime rates — one mistake costs money, a correction run and trust.
- Thai law as tested code. Withholding tax, social security, provident fund and Labour Protection Act overtime, as pure functions with worked examples.
- Whoever prepares a run cannot approve it, enforced by the API.
- No export until the figures reconcile. Every run in the period approved, and the payslips adding up to the run totals to the baht (CW-044).
- Approvers see why the total moved. The optional assistant narrates the variance against last period, and a test fails on any figure it invents (CW-040).
- Payslips you can explain a year later, with employer contributions kept apart from deductions.
- Not yet: the PND 1, social-security and bank-transfer files.
- Accounts bound to a device. The first phone binds; a punch from another is flagged, and only HR can move the binding, audited (CW-024).
- Flag for review, never refuse. Punches outside the fence or with a suspicious location are accepted and flagged — whoever turned up can always clock in.
- Offline punches kept safe. An encrypted on-device queue, rooted-device detection, and late deliveries that need confirming before they count (CW-025).
- Rosters that lateness is measured against, assignable a department at a time (CW-010).
- A summary for managers instead of a list of flags: flagged punches grouped by location, so a fence drawn too tight reads differently from fraud (CW-039).
- One approval engine, nine request types — leave, overtime, expenses, attendance corrections, resignations, requisitions, offers, payroll runs and document requests.
- Routes that follow the organisation — line manager, department head, role or named person, with amount thresholds and delegation.
- Approve from a phone. Email and push go out through a transactional outbox that retries, backs off and dead-letters.
- Leave balances that cannot go negative. Pending days are reserved on filing; weekends and public holidays are never counted.
- Self-service certificates, issued as PDFs with a verification code (CW-008).
- National IDs and bank accounts encrypted at rest and masked to the last four digits.
- A password alone is not a session for anyone who can read them or run payroll.
- An audit trail nobody can rewrite, append-only by database trigger.
- Keep only what you need — consent on application, candidates erased after 12 months, leavers redacted once past retention (CW-015).
- Nothing leaks through the logs. No salary, national ID, bank account, token or query string reaches a log line, and an e2e test proves it (CW-050).
- Uploads scanned for malware before they are stored.
- Applicant to employee in one click — requisition, public careers page, auto-graded assessment, scorecards, and an offer that becomes the employee record.
- Fairer reviews — weighted KPIs, check-ins and calibration by HR.
- Benefits that flow into payroll without re-keying (CW-009).
- Offboarding with a clearance checklist and exit interview.
- Thai and English throughout, web and app (CW-016).
Three deployables, one database:
| Admin console | React 19 + Vite. Everything HR, payroll and managers do. |
| Employee app | Flutter. Clock in/out, leave, payslips, approvals — offline-tolerant. |
| API | NestJS modular monolith over PostgreSQL 16. |
flowchart TB
W["Admin console<br/><i>React 19 · Vite</i>"] --> API
M["Employee app<br/><i>Flutter · offline-tolerant</i>"] --> API
C["Public careers page"] --> API
API["<b>NestJS API</b> — modular monolith<br/>JWT → permissions → rate limit<br/>feature modules over pure <i>domain/</i> rules"]
API --> DB[("PostgreSQL 16<br/><i>records · job locks · rate-limit counters · outbox</i>")]
DB --> OUT["Transactional outbox<br/><i>at-least-once, backs off, dead-letters</i>"]
OUT --> MAIL["SMTP"]
OUT --> PUSH["FCM push"]
API -. optional .-> CLAM["clamd<br/><i>scans uploads</i>"]
API -. off by default .-> LLM["Anthropic<br/><i>HR assistant</i>"]
Everything a queue, a lock server and a cache would normally do, PostgreSQL does
here: FOR UPDATE SKIP LOCKED for the outbox, advisory locks so only one replica
runs each scheduled job, and a counter table for shared rate limits. That is the
whole reason a second instance is a setting rather than a project.
No Redis, no message broker, no Kubernetes. Running several API instances needs
one setting (THROTTLE_STORAGE=postgres) rather than another service to operate
— an HRIS that needs a Kafka cluster to send a leave notification is one nobody
can self-host.
One deployment serves one organisation. Every table carries organizationId
and every query filters on it, but that is defence in depth inside a single
install, not a tenant boundary: nothing in the test suite exercises two
organisations sharing a database, so nothing should depend on it.
git clone https://github.com/SuruchBoss/Cwork.git
cd Cwork
cp .env.example .envFill in the secrets .env asks for — compose refuses to start without them:
openssl rand -base64 48 # JWT_ACCESS_SECRET, JWT_REFRESH_SECRET
openssl rand -base64 32 # FIELD_ENCRYPTION_KEY
# plus POSTGRES_PASSWORD, anything you likeThen:
docker compose up -d --build
docker compose run --rm --build migrate # apply migrations
docker compose run --rm migrate npm run db:seed # demo data — evaluation onlyOpen http://localhost:8080.
The seed does not stop at an org chart: it runs last month's payroll through the real calculator, books leave that real approvers approved, and leaves a hiring pipeline mid-flight and two requests waiting in an inbox. Every page has something on it, because a console full of "nothing here yet" tells you nothing about whether the thing works.
| As an employee | dev2@cwork.example — password only |
| As HR | hr.manager@cwork.example — plus a 2FA code |
Every demo account shares one password, which db:seed generates and prints
once, at the end of its output — or choose it yourself with
docker compose run --rm -e SEED_PASSWORD=… migrate npm run db:seed. There is no
default: a password printed here would be the same on every installation that
did not change it.
Admin accounts genuinely require a second factor too, so db:seed also prints a
demo TOTP secret and a QR link. Add it to any authenticator app once and every
admin account works.
Migrations run through the
migrateservice, notexec api. The API image is pruned to production dependencies and ships no Prisma CLI. That service sits behind a compose profile, soupnever starts it.
Running without Docker
# API — needs PostgreSQL 16 with the pgvector extension (the first migration
# creates it; `pgvector/pgvector:pg16` is what CI and compose run)
cd backend
cp .env.example .env # set DATABASE_URL and the secrets
npm install
npx prisma migrate deploy && npm run db:seed
npm run start:dev # → http://localhost:3000
# Console
cd ../web
npm install && npm run dev # → http://localhost:5173, proxies /api
# Mobile — the dart-define skips the connect screen; without it, a debug
# build lets you type http://10.0.2.2:3000 there
cd ../mobile
flutter pub get
flutter run --dart-define=API_BASE_URL=http://10.0.2.2:3000/api/v1The interactive API docs live at /api/docs whenever NODE_ENV is not
production — which is what compose defaults it to, so set
NODE_ENV=development in .env if you want them.
The five minutes above are demo data: a published password, a published two-factor secret, eight fictional employees. A real install skips the seed entirely.
docker compose up -d --build
docker compose run --rm --build migrate # apply migrations
docker compose run --rm migrate npm run db:init # your organisationdb:init asks for an organisation name, a timezone and the first
administrator's email, and creates exactly that: one organisation, the eight
system roles, one account. Nothing else — no departments, no demo rows, nothing
to clean up afterwards.
If the terminal is not where you want to type all that:
docker compose run --rm migrate npm run db:init -- --webprints a single-use token and you finish at http://localhost:8080/setup. That token is the entire security model. Minting one needs shell access to the server, which is the one thing a stranger who finds a fresh deployment does not have — so the wizard cannot be claimed by whoever reaches it first. It lasts an hour and works once.
Either way the first administrator holds every permission there is, so Cwork makes it enrol a second factor before its first session: have an authenticator app to hand. Neither the CLI nor the wizard ever prints a TOTP secret — you enrol it yourself, once.
Running db:init a second time on a database that already has an organisation
refuses, and so does db:seed: the demo data will not install itself beside a
real company by accident.
deploy/nas/ installs Cwork on a NAS that runs containers, beside whatever
already runs there: ./check.sh first, which only reads and changes nothing,
then install, backup, restore, update, rollback and change-address scripts, with
HTTPS on a single port. A backup refuses to go on the same disk as the data,
and show-key.sh prints the encryption key on request, to be kept in a password
manager, never beside the backups. It was rehearsed on
a clean Docker host (notes); the Synology DSM
screens, the certificate on Windows, a scan from outside the office and the
app-on-a-PC layout have not been tried on real hardware yet. The step-by-step
guide is in Thai: docs/guide/nas-install.th.md.
There is one Android app, the same for every company. Each employee connects it to your server, and nobody builds anything for your company.
- Serve Cwork over HTTPS, with a certificate phones trust: a reverse proxy such as Caddy or nginx in front of port 8080, with a Let's Encrypt certificate, is enough. The app refuses plain HTTP, because passwords and national IDs travel over it.
- In the console, open Employees → Mobile app. It shows a QR code and a
link to your server's install page,
https://<your server>/app. Print it, post it in the staff chat, or show it at the HR desk. - The employee scans it with the phone camera. The install page offers the APK from the release your server runs. Android asks once to allow installs from the browser. Play Protect may also warn that the app is not from the Play Store, which is true.
- They open the app and connect. They can scan the same code, tap Open the app on the install page, or type your server's address. The app shows which server it will connect to and asks first. It connects only if the server is Cwork, over HTTPS. Then they sign in as usual.
A later release installs over the app. The employee stays signed in, and any clock-in still queued offline is sent as before.
iPhone is not supported yet. Publishing an iOS app needs an Apple Developer Program membership, and that decision is still open. Until then, iPhone users cannot install the app.
The release workflow builds and signs the APK. For maintainers, the signing key and the release steps are in docs/mobile-release.md.
The interface is Thai by default and switches to English (CW-016) — in both the console and the employee app, every label, form, status and validation message flips between the two from one switch (a header control on the web, the profile screen on mobile), and Thai stays the default. Every image below is a real screenshot of the seeded demo company on the English interface — the console in a browser, the employee app on a 390×844 phone; the Thai README shows the same screens in Thai. Names, leave types and anything else the company typed in stay Thai, as they would for a Thai company: the interface is translated, its content is not. They are taken by
capture.mjsandcapture-mobile.mjs, so they can be retaken whenever a screen changes.
Employee records with encrypted national IDs and bank accounts, an org chart, employment history, and resignation with a clearance checklist and exit interview.
A company that already keeps its people in Excel or another system brings them in from a spreadsheet, with the leave each person has already taken this year. Every problem is listed by row and column before anything is saved, and a Thai Excel CSV reads as Thai. A single new hire is added from the directory's "Add employee" form, with the same checks.
Passports and work permits are watched. An expiring-documents page lists foreign workers' documents due within 90, 60 or 30 days or already expired, and whoever may edit the employee is told at each point. The document number is never shown.
Thai statutory leave types, seniority-tiered accrual, half and hourly days, and a balance ledger that reserves pending days — so two overlapping requests cannot both fit inside one remaining day.
Friday to Monday costs two days, not four. Weekends and public holidays are excluded, not charged.
Clock in/out with geofencing and anti-fraud flags, shift rosters, corrections, and overtime at Labour Protection Act rates.
A punch outside the fence is flagged, never rejected. An employee must always be able to prove they turned up; HR reviews the flag afterwards.
A fingerprint scanner's export comes in through HR's review. Two times a day count as in and out; any other day waits for HR with every raw time on screen, and days in a paid period are locked. Guide (Thai)
Effective-dated salary, Thai withholding tax and social security, benefits, expense reimbursement — and payslips you can still explain a year later.
Whoever calculates a run cannot approve it, enforced by the API rather than by policy.
Starting in September withholds the right tax in September. A company that moves to Cwork mid-year imports what it paid each person from January, and the first run withholds exactly what it would have if the whole year had run here. Those months stay the company's own for the year-end filings.
A cash payday is printed, not worked out by hand. An approved run prints payslips one, two or four to an A4 sheet, a wage-receipt sheet to sign, and a cash-preparation sheet with every note and coin per person and the amount to withdraw. Guide (Thai)
The filings start from payroll's own numbers. The monthly social security form's figures (สปส.1-10) download as CSV, Excel or PDF (guide), and the accountant's export adds each person's tax ID and income type for the ภ.ง.ด.1. A compare page sets the totals that were filed beside Cwork's, and a parallel-run check marks every person and column that differs from your own figures. These prepare and check a filing; they do not submit one.
One declarative engine serves leave, overtime, expenses, attendance corrections, resignations, requisitions, offers, payroll runs and document requests. Policies resolve approvers by line manager, department head, role or named user, and carry conditions such as amount thresholds.
Requisitions through a public careers page, PDPA-consented applications, auto-graded assessments, interviews with scorecards, and offers that convert into employee records. Review cycles with weighted KPIs and calibration. Certificates and letters employees can request for themselves.
Answers policy questions from your own documents, checks balances, and files leave and document requests — scoped so it can only ever see the asker's own data.
It is optional and off by default. Policy search falls back to PostgreSQL full-text plus trigram matching, which needs no embeddings and copes with Thai's lack of word boundaries.
Flutter, for everyone who never opens the console. Clock in and out with geofencing, request leave, read a payslip, approve what is waiting, ask the assistant.
Built for a phone that loses signal: punches queue in durable storage and replay on reconnect, carrying a client-generated id so a retry cannot become a second punch.
That payslip is the "explain it a year later" claim in practice: earnings, deductions, and employer contributions kept visibly separate from what came out of the employee's pay.
More screenshots — sign-in and 2FA, expenses, benefits, offboarding, org chart, audit, dark theme, phone width
Sign-in, and the second factor an admin account cannot skip
Expenses, benefits and offboarding
Org chart and the append-only audit trail
More of the employee app — sign-in, payslips, profile, and filing leave
Dark theme, and the console at phone width
Business rules are pure functions. Leave arithmetic, attendance derivation,
Thai tax, KPI scoring and assessment grading live in domain/ directories with
no database, no framework and no I/O. That is why 495 domain tests run in under ten
seconds — and why "why was I charged 2.5 days?" is answered by reading one
function instead of a query plan.
Salary is never prorated by attendance coverage. A monthly-salaried employee is paid the full month minus explicit unpaid leave and absence. Days that simply have not been closed out — future dates, or a clock-in rollout still in progress — must not reduce pay. Getting this wrong silently shorts people, which is the worst class of payroll bug.
A fresh deployment cannot be claimed by whoever finds it first. Between
docker compose up and the moment setup finishes, an install is reachable and
unowned — and a scanner sweeping the port beats the person still reading startup
logs. So there is no "if no organisation exists, let anyone through": the first
administrator is created either by a CLI that needs a shell on the server, or by
a wizard holding a single-use token only that CLI can mint.
A notification is written in the same transaction as the thing it is about.
Approve a leave request and the row, the balance and the notification commit or
roll back together — there is no window where someone is told about a decision
that was rolled back. Delivery is a separate poll over the outbox table with
FOR UPDATE SKIP LOCKED, so a mail server being down cannot fail an approval.
The audit trail is append-only in the database. A trigger raises an exception
on UPDATE or DELETE against audit_logs and attendance_punches. A
compromised application account can add entries but cannot rewrite history.
Privileged accounts cannot sign in with a password alone. Anyone who can read national IDs, run payroll or hand out permissions needs a second factor. TOTP is implemented against RFC 6238's own test vectors rather than pulled in as a dependency, and a code cannot be spent twice even inside its validity window.
Uploads are scanned before they are stored. Résumés arrive from a public careers page — the least trusted input the system takes. The bytes go to clamd first, so malware is never written anywhere for a later change to expose. And a scanner that is not working is never a pass: unreachable, timed out, or a reply that cannot be parsed all leave the file held and undownloadable.
The assistant's tools take no employee id. Every one resolves the subject from the authenticated principal, so there is no parameter a prompt injection could set to read someone else's payslip. The blast radius of a fully compromised model is bounded by what that user could already see. See ADR-0004.
Cwork works on its own and always will. It is also the system of record for people, attendance, leave and payroll in a group of projects by the same maintainer that touch one restaurant chain from different sides:
| PaynEat POS | Point of sale at a branch |
| PaynEat ERP | Supply side: procurement, plant, distribution, stock, cost |
| Cwork | People, attendance, leave, payroll, labour cost per cost centre |
| SherWhyve | Investigates technical incidents across the systems above. Private, no public repository. |
No system reads another's database; they integrate through versioned APIs and events. A site has the same location code in all of them (ADR-0006).
The ERP has a free edition and a paid one; the feature that will consume Cwork's labour aggregates is in the paid one. Since 2026-10-10 Cwork has the same kind of split — see Editions. The labour data contract stays in the free edition: it is a documented public interface any other consumer may implement against, and Apache 2.0 has always allowed anyone, the maintainer included, to build a commercial product on it.
None of this is a dependency. Installing Cwork on its own gives you the whole product, and every convention adopted for the ecosystem has to earn its place in a standalone install first — the reasoning is in spec.md § Agreed direction.
backend/ NestJS API — modular monolith over PostgreSQL
web/ React 19 + Vite admin console
mobile/ Flutter employee app
docs/ Spec, architecture, security, data model, API, ADRs, backlog
landing/ The overview page, published to GitHub Pages
.github/ CI workflow, issue and pull-request templates
| Specification | What the system does, module by module — the reference for what "correct" means |
| Backlog | Open work, prioritised, with acceptance criteria |
| Architecture | How the pieces fit, and what is deliberately absent |
| Security | Auth, encryption, audit — and an honest list of gaps |
| Personal data | What is held about people, for how long, and how to take it out |
| Privacy notice (ไทย) | A draft notice to hand to employees |
| Data model | Schema patterns and the hand-written SQL |
| API reference | Endpoints, error codes, conventions |
| Payroll: Thai rules | Tax brackets, allowances, OT multipliers |
| The HR assistant | Tools, guardrails, data flow |
| Operations | Deploy, backup, scheduled jobs, the Prisma drift trap |
| Releasing the mobile app | The signing key, the APK in each release, what an update keeps |
| ADRs | Decisions that were not obvious |
The UI ships in Thai and English — switched from the header, Thai by default
(CW-016) — and payroll implements Thai rules — PIT withholding, social security,
Labour Protection Act overtime. Nothing in the architecture is Thailand-specific:
the tax rule set is data (THAI_TAX_RULES_2026), leave types are configuration,
and overtime multipliers are per-organisation settings. The English locale added
a dependency-free i18n layer keyed on the English source string, so a third
language is a catalogue rather than a code change; adding another jurisdiction
means a new rule set and that translation pass, not a rewrite.
Working and verified end to end — sign-in through payroll. 788 backend unit tests, 322 web, 101 mobile, plus a 447-check end-to-end suite that drives the real API over HTTP in CI, and the console exercised in a real browser against the live API.
Second-factor authentication, upload scanning, shared rate limiting and email/push delivery are all in place. What is left is tracked in the backlog, P0 first.
Still not production-ready without work. Before running real payroll, read the gaps in docs/security.md. In short: no ภ.ง.ด.1 filing file (the accountant's export is a working file for whoever files, not a submission), the employee app cannot register for push yet, retention is a policy you have to enforce by hand (how), the interface covers only Thai and English, and this code has never had a penetration test.
The Thai payroll and social-security rules have not been reviewed by anyone qualified. They were written from published sources and unit-tested against hand-worked examples, which proves the code computes what its author believed the rules to be — not that the belief is right. Check the figures against your own before a real run. If you have the standing to review them properly, issue #36 is open for it.
Releases are tagged and recorded in CHANGELOG.md. Install a
tag rather than main if you want to be able to say which version you are
running — the tag list is the
authority on what exists:
git clone --branch <tag> https://github.com/SuruchBoss/Cwork.gitThis is 0.x, and breaking changes are allowed. They will be in the changelog with what to do about them, but there is no long-term support branch and no promise of a painless upgrade path until 1.0. Migrations are forward-only; take a database backup before applying any.
1.0 means the schema and the API are stable enough to promise that. It is not close, and pretending otherwise would be the more expensive mistake.
The code in this repository was written by an AI agent, working under
direction, over a small number of days. Several commits add thousands of lines
at once. That is visible in git log to anyone who looks, and it changes what
you should check before trusting any of it — so it is said here rather than left
to be inferred.
What was not generated: the architecture, the security model, the priorities, and the arguments behind them. Those were decided by a person, recorded as they were settled, and are readable in spec.md § Agreed direction and in the ADRs. Where a ticket's own premise turned out to be wrong, the correction is written down in the backlog's Done table next to what was built — that record is the point.
What it implies about review:
- Every suite passes, in CI, on every push — 788 backend unit tests, 322 web, 101 mobile, 447 end-to-end checks against the real API over HTTP.
- The decisions are documented and the reasoning is recoverable.
- No independent human has read every line. Tests passing and a design being defensible are not the same thing as a review, and this has had the first two and not the third.
The history has not been rewritten to look otherwise, and it will not be. Rewriting it would be the actual dishonesty.
This changed on 2026-10-10. Until then this README said Cwork was entirely Apache 2.0 and would stay that way. The maintainer reversed that before any company ran Cwork in production, and says so here rather than leaving it to be discovered: charging for something people already use would be breaking a promise; offering paid features on top of a free edition that does the job is not, and the place to draw that line is before anyone depends on it. There were no outside contributors at the time, so nobody else's work changes licence.
- Everything already here stays Apache 2.0, for good. All code in this
repository up to the commit that published this section, and everything
outside
ee/after it. - Free, always: payroll that is correct for Thailand, including the updates when social security, tax or labour law changes — employees are harmed when a deduction is wrong, so that never sits behind a licence; leave, attendance, the PDPA tools, data export, security fixes and bug fixes; one company with no limit on the number of employees.
- Cwork Pro is new code in
ee/under the Elastic License 2.0. Candidates, not promises: online filing to the Social Security Office and the Revenue Department, bank transfer files, several companies in one install, single sign-on, deeper analytics, LINE notifications. - How Pro is sold. An annual subscription for an installation you run yourself: your own IT installs and operates Cwork, support is online, and a remote installation can be bought; there is no on-site service and no hosted edition. Letting the subscription lapse leaves the free edition fully working, and a change in the law is released to both editions at the same time. Prices are not set yet.
- How the two meet. Code outside
ee/never imports from it. A Pro licence key is checked offline, and an expired key never locks anyone's data or stops a payroll run.
Contributions welcome — see CONTRIBUTING.md. The backlog is the list of open work, with the ones marked good first issue called out; they tend to be leave-policy variants, payroll edge cases, or a jurisdiction other than Thailand.
Commits in a pull request from a fork need a sign-off (git commit -s) under the
Developer Certificate of Origin — a statement that you had the right to
submit the change, checked in CI. There is no CLA: a contributor licence
agreement is a barrier in front of a one-line fix, and the DCO answers the
question a CLA is usually reached for. The accepted consequence is that
contributions outside ee/ are Apache-2.0 for good: without a CLA they cannot be
relicensed, and they will not be. ee/ takes no outside contributions — see
Editions.
Apache 2.0 for everything outside ee/; the Elastic License 2.0
for code in ee/ (Cwork Pro), with its own licence file in that directory once
the first Pro code lands.
Copyright 2026 Suruch Chakrapeesirisuk; see also NOTICE.
Every source file starts with its copyright line and an SPDX identifier —
SPDX-License-Identifier: Apache-2.0 outside ee/, Elastic-2.0 inside it —
checked in CI; applied database
migrations are exempt. Outside contributions are signed off under the Developer
Certificate of Origin — see CONTRIBUTING.md.































