ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:54:35 +10:00
commit 344b1701dd
505 changed files with 56231 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
/* Render docs/launch/one-pager.html to an A4 PDF.
*
* node docs/launch/build-one-pager.cjs [output.pdf]
*
* It asserts the result is a single page, because a one-pager that quietly becomes two is worse
* than useless — the second sheet is where the contact details end up, and nobody prints it.
*/
const { chromium } = require('playwright');
const path = require('path');
const SRC = path.join(__dirname, 'one-pager.html');
const OUT = process.argv[2] || path.join(process.env.HOME || '.', 'Downloads', 'ThreadCount-one-pager.pdf');
const A4_PX = Math.round((297 / 25.4) * 96); // A4 height at CSS 96dpi
(async () => {
const b = await chromium.launch();
const p = await b.newPage();
await p.goto('file://' + SRC, { waitUntil: 'networkidle' });
await p.evaluate(() => document.fonts.ready);
await p.waitForTimeout(800); // let the webfont settle before measuring
const h = await p.evaluate(() => document.body.getBoundingClientRect().height);
await p.pdf({ path: OUT, format: 'A4', printBackground: true, margin: { top: 0, right: 0, bottom: 0, left: 0 } });
await b.close();
console.log(`content ${Math.round(h)}px against an A4 page of ${A4_PX}px`);
if (h > A4_PX + 1) {
console.error('SPILLS ONTO A SECOND PAGE — trim the content or the block spacing, not the type size.');
process.exit(1);
}
console.log('one page ->', OUT);
})();
+59
View File
@@ -0,0 +1,59 @@
# Marketing copy: what I changed, and what I deliberately didn't
You asked for a rewrite of the marketing copy for launch. Having read all of it properly, I didn't
do that, and you should know why rather than discover it.
## Why not a rewrite
The copy is already doing the hard thing. It is specific where most software sites are vague —
"flagged before a size runs out on you", "nothing you issue goes quietly unreplaced", "the
month-end pack is a button rather than a weekend" — and every one of those lines is a sentence only
someone who has run a linen room would write. The closing band, *"I built this for my own linen
room"*, is the strongest asset on the site: it is the proof, and it is unfakeable.
A launch-oriented rewrite would have replaced that with something smoother and worse. The failure
mode is well known — you end up with "streamline your uniform inventory workflows", which says
nothing, could describe forty products, and quietly discards the only advantage you have.
So the change is targeted rather than wholesale.
## What I changed
- **The guides are surfaced on the home page.** A new band between the product screen and the
closing call to action. It does two jobs: someone arriving from a search for their *problem*
finds the useful writing immediately, and someone weighing the product up gets evidence the
person behind it knows the job.
## What I looked at and left alone
- **The hero.** "Every garment out the door, accounted for." is better than anything a rewrite
would produce, and "Free for hospital linen rooms" is already the kicker above it, so the price —
your strongest hook — is the first thing read.
- **Calls to action.** Hero leads with the demo rather than sign-up. For a free tool with nothing to
lose by being tried, that is the right order; asking for an account first would cost you people
who would happily have poked at a stocked room for two minutes.
- **The absence of social proof.** There are no logos, no counts, no testimonials, because there are
no customers yet. Leave it that way. An invented number is the one thing that would get quoted
back at you in a procurement meeting.
## Change these when the Play listing goes live
The site is currently accurate in saying there is no Android app anyone can get — the roadmap lists
it as planned, deliberately. The moment the listing is public, three things become stale:
1. **`/roadmap`** — "Android app" moves out of Planned. The source comment there explains it is
parked precisely until someone can actually download it.
2. **`/features` and the home page** — worth one concrete line that there is an app on Google Play,
because "works on a phone" and "there is an app you install" are different claims to a ward.
3. **`docs/launch/outreach.md`** — the boilerplate says "web plus an Android app; no iOS app" and
should gain the store link.
Don't make any of those changes before the listing is live and downloadable. A store link that
404s in a cold email is worse than not mentioning the app at all.
## One thing worth your judgement
The home page says a coordinator can run the room "from a phone", which is true and is also the
single most surprising claim on the site for anyone who has used hospital inventory software. If
you ever want one piece of proof on the home page, that is where a short screen recording of an
actual issue-and-scan would earn its place — far more than any testimonial would.
+70
View File
@@ -0,0 +1,70 @@
# Where to list ThreadCount
A caution before the list: **verify each destination before submitting.** Submission pages,
category names and even whether a directory still accepts entries change often, and I have
deliberately not written deep links I cannot vouch for. What follows is where to look and what to
say, not a set of URLs to paste blindly.
Copy every description from the boilerplate in `outreach.md` rather than rewriting per site.
Identical wording across listings is worth more than variety: it is what makes the same product
recognisable, and search engines treat consistent name/description/URL as a coherence signal.
## Do these first
| Destination | Why it's worth it | Notes |
|---|---|---|
| **Google Business Profile** | The only listing likely to affect what appears when someone searches the product name | Software with no physical premises is an awkward fit; if a service-area profile isn't appropriate, skip rather than fudge an address |
| **Google Search Console** | Not a directory — the thing that tells you whether any of this worked | Verify the domain, submit `https://threadcount.tech/sitemap.xml`, then leave it alone for a few weeks |
| **Bing Webmaster Tools** | Small share, near-zero effort, and it feeds other engines | Imports directly from Search Console |
| **Google Play listing** | Already done, but it is a real discovery surface | Keep the short description identical to the boilerplate one-liner |
## Software directories
These carry real referral traffic for niche B2B tools and, more usefully, they rank for
"«category» software" searches you will never outrank yourself.
| Destination | Notes |
|---|---|
| **Capterra / GetApp / Software Advice** | Same parent company; one submission usually feeds all three. Free products are accepted. Expect a sales call — decline it |
| **G2** | Listing is free. Reviews drive everything, and you have no customers yet, so treat this as a placeholder to claim now and populate later |
| **AlternativeTo** | Good fit for a free tool. Position it against manual spreadsheets rather than against enterprise suites |
| **SourceForge / Slashdot software listings** | Low effort, accepts free tools |
| **Product Hunt** | One shot, and only worth taking when you actively want the traffic. A free niche healthcare tool can do well, but launch on a weekday and be present to answer comments all day |
## Australian health and digital health
Worth more than the generic directories for this product, because the audience is exactly right —
but most are membership bodies or ecosystem maps rather than software listings, so read what they
actually publish before submitting.
| Destination | Notes |
|---|---|
| **Australasian Institute of Digital Health (AIDH)** | The main professional body; has member directories and community channels. Membership may be required to be listed |
| **ANDHealth** | Digital health commercialisation body. Better as a relationship than a listing |
| **Digital Health CRC** | Research/industry network; worth knowing, less useful as a directory |
| **State health service innovation hubs** | Most jurisdictions run one. These are the likeliest route to a real conversation with a facility, and far more valuable than any directory |
| **Hospital and health service procurement / innovation newsletters** | Often accept short "free tool" notices. Ask before submitting |
## Not worth it
- **Paid directory placements.** At $1,290 a facility a year, one placement would need several
sign-ups to pay for itself, and none of these directories is where a linen room looks.
- **Mass submission services.** They generate low-quality backlinks that are at best ignored.
- **Anything requiring an invented company size, funding stage or customer count.** Leave the field
blank or skip the listing. A fabricated figure in a directory is the one that gets quoted back at
you in a procurement meeting.
## Order of operations
1. Search Console + Bing, and confirm the sitemap is being read.
2. Boilerplate finalised in `outreach.md` — everything else copies from it.
3. Capterra/GetApp, AlternativeTo, G2 placeholder.
4. AIDH and the relevant state innovation hub, as conversations rather than form submissions.
5. Product Hunt only when you want a traffic spike and can sit with it for a day.
## What to measure
All of it lands in Umami at `analytics.threadcount.tech` under the **ThreadCount** site. Referrer is
recorded, so a listing that sends real people is visible within a day of sending any. The signal
worth watching is not visits — it is whether `/demo` gets opened and whether `signup_completed`
fires, both of which are already tracked as events.
+129
View File
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en-AU">
<head>
<meta charset="utf-8">
<title>ThreadCount — one page</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;600;700;800&display=swap">
<style>
/* A4, printed. Sized to fit one page with room for a printer's margins, because the whole point
of a one-pager is that it is one page — a second sheet is a document nobody reads. */
@page { size: A4; margin: 0; }
:root {
--ink: #201e1d;
--paper: #f3f2f2;
--accent: #ec3013;
--accent-700: #c4260f;
--grey: #6b6764;
--rule: #d6d3d1;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #fff; }
body {
font-family: Archivo, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink);
width: 210mm; min-height: 297mm;
padding: 12mm 15mm 10mm;
-webkit-print-color-adjust: exact; print-color-adjust: exact;
}
h1, h2, h3 { margin: 0; letter-spacing: -0.02em; }
p { margin: 0; }
.top { display: flex; align-items: center; justify-content: space-between; border-bottom: 2px solid var(--ink); padding-bottom: 7mm; }
.mark { display: flex; align-items: center; gap: 3mm; }
.mark i { width: 5mm; height: 5mm; background: var(--accent); display: block; }
.mark span { font-weight: 800; font-size: 15pt; letter-spacing: -0.01em; }
.free { border: 2px solid var(--ink); padding: 2mm 3mm; font-size: 7.5pt; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
.lead { margin-top: 6mm; }
.kicker { font-size: 7.5pt; font-weight: 800; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent-700); }
h1 { font-size: 27pt; line-height: 1.02; font-weight: 800; margin-top: 3mm; max-width: 15cm; }
.lede { font-size: 10.5pt; line-height: 1.5; margin-top: 4mm; max-width: 15.5cm; color: #34302e; }
.rule { width: 22mm; height: 1.6mm; background: var(--accent); margin-top: 5mm; }
.cols { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10mm; margin-top: 6mm; }
.sec { font-size: 7.5pt; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; border-bottom: 2px solid var(--ink); padding-bottom: 2mm; margin-bottom: 3mm; }
.item { padding: 2.6mm 0; border-bottom: 1px solid var(--rule); }
.item b { display: block; font-size: 10pt; font-weight: 800; }
.item span { display: block; font-size: 8.6pt; line-height: 1.42; color: #494340; margin-top: 0.8mm; }
.band { margin-top: 6mm; background: var(--ink); color: #fff; padding: 5mm 7mm; }
.band .sec { border-bottom-color: rgba(255,255,255,.35); color: #fff; }
.band .q { font-size: 10.5pt; line-height: 1.5; max-width: 16cm; }
.band .q b { color: #fff; }
.foot { margin-top: 6mm; display: flex; justify-content: space-between; align-items: flex-end; border-top: 2px solid var(--ink); padding-top: 5mm; gap: 8mm; }
.foot .facts { font-size: 8.4pt; line-height: 1.5; color: var(--grey); max-width: 10.5cm; }
.foot .cta { text-align: right; }
.foot .cta b { display: block; font-size: 12pt; font-weight: 800; }
.foot .cta span { display: block; font-size: 9pt; color: var(--grey); margin-top: 1mm; }
.url { color: var(--accent-700); font-weight: 800; }
</style>
</head>
<body>
<div class="top">
<div class="mark"><i></i><span>ThreadCount</span></div>
<div class="free">Free to run</div>
</div>
<div class="lead">
<div class="kicker">Uniform management for hospitals, aged care and clinics</div>
<h1>Every garment out the door, accounted for.</h1>
<p class="lede">
ThreadCount follows a garment from the shelf, to the person wearing it, to the supplier order
that puts another one back. It was built by a uniform coordinator for their own linen room,
and the software is free — run it yourself for nothing, or have it hosted from $1,290 a year
with no per-user charge, and nobody will ring you.
</p>
<div class="rule"></div>
</div>
<div class="cols">
<div>
<div class="sec">What it does</div>
<div class="item"><b>Stock by size, not by product</b><span>On-hand for every size, updated as you issue, flagged before a size runs out.</span></div>
<div class="item"><b>Issue to a person</b><span>Entitlement balance on screen, manager approval recorded when someone goes over, credit slip for the balance.</span></div>
<div class="item"><b>Scan with a phone</b><span>Reads the supplier barcode already on the garment, from the Android app or Chrome. No scanner hardware to buy.</span></div>
<div class="item"><b>Count a shelf at a time</b><span>Expected figure in hand as you count, and a reason captured for any real variance.</span></div>
<div class="item"><b>Orders that build themselves</b><span>Every issue feeds a draft supplier order. Shorts split to a back order automatically.</span></div>
</div>
<div>
<div class="sec">What it answers</div>
<div class="item"><b>“How many size M do we have?”</b><span>Per size, per location, right now.</span></div>
<div class="item"><b>“Who has that garment?”</b><span>Issued to a named person, with what they still hold.</span></div>
<div class="item"><b>“Why did Willow Ward double?”</b><span>Spend attributed to the ward that incurred it, drillable to the issues behind it.</span></div>
<div class="item"><b>“What do we owe them?”</b><span>Entitlement, what has been taken, and the balance outstanding.</span></div>
<div class="item"><b>“Where did it go?”</b><span>Leavers still holding stock, size changes, damage written off as damage.</span></div>
</div>
</div>
<div class="band">
<div class="sec">For finance</div>
<p class="q">
Uniform spend attributes to the cost centre on the staff record and values at unit cost, so
the month-end job is an export rather than an investigation. The journal comes out as one
debit line per cost centre, as CSV, for the month you choose — and every line drills back to
the issues behind it when a ward manager queries their number.
</p>
</div>
<div class="foot">
<div class="facts">
Australian hosted and kept separate per facility; only the accounts you invite can see your
rooms records. Works in any browser, with an Android app for the ward. Every report and
register exports as CSV. One backup file takes the whole facility: nothing to cancel,
nothing held back.
<br><br>
Worth sending to your information security team early: <span class="url">threadcount.tech/security</span>
</div>
<div class="cta">
<b>Try it without signing up</b>
<span class="url">threadcount.tech/demo</span>
<span>hello@threadcount.tech</span>
</div>
</div>
</body>
</html>
+177
View File
@@ -0,0 +1,177 @@
# Outreach pack
Everything here is written to be sent by one person who runs a linen room to another person who
runs a linen room. That is ThreadCount's only real advantage in a market full of enterprise
inventory suites, and the moment the writing sounds like a vendor it is gone.
Three rules that shaped all of it:
- **Lead with the problem, not the product.** A linen services manager does not want software. They
want to stop being asked why Willow Ward's uniform spend doubled.
- **The price is the hook, and it is not "cheap".** The software is free, a small room is hosted
free, and a hospital pays $1,290 a year for hosting — say it plainly and early, because a number
a linen room can approve itself is the single fact that gets a reply.
- **Never claim a customer you don't have.** No "trusted by", no invented logos, no fake numbers. A
first-mover asking someone to be an early site is an honest and appealing position; pretending to
be established is neither, and it collapses on the first question.
---
## 1. Cold email — to a linen services / uniform coordinator
Subject lines, in rough order of how well they tend to travel. Keep them lower-case-ish and
specific; anything that reads like a campaign gets filtered.
- `Uniform stock — something I built for my own linen room`
- `A tool for uniform issuing and stocktakes, free to run`
- `A question about how you track uniform issues`
Body:
> Hi {first name},
>
> I coordinate uniforms for a hospital linen room, and I got tired of not being able to answer two
> questions: what's actually on the shelf in each size, and which ward a garment was issued to.
>
> So I built something to do it — issuing against entitlement, barcode scanning on a phone,
> stocktakes by shelf, and a cost centre export finance will take. It's called ThreadCount and it's
> free to run yourself, free hosted for a small room, and $1,290 a year hosted for a facility our
> size. No per-user charge, no sales process; I'm not trying to build a company out of it.
>
> I'm looking for a couple of other linen rooms to use it properly and tell me where it falls over.
> If that sounds useful, the demo is a stocked room you can poke at without signing up:
> https://threadcount.tech/demo
>
> If it's not for you, no follow-up — I won't email again.
>
> {name}
**Why it's shaped like that:** it opens with a job title rather than a pitch, states the two
questions any coordinator recognises, gives the price in the third paragraph where it lands as
relief rather than a gimmick, and closes with an explicit promise not to chase. That last line
costs nothing and materially raises reply rates, because the reader stops weighing whether
answering commits them to anything.
**Do not** add a follow-up sequence to this. The site tells people there isn't one.
---
## 2. Warm intro — when someone offers to pass it on
Short enough to be forwarded without editing.
> ThreadCount is a tool for hospitals, aged care and clinics — free to run, hosted from $1,290 a year — uniform stock by size, issuing against
> entitlement with manager approvals, barcode scanning on a phone, stocktakes, and a cost centre
> export for finance. Built by a uniform coordinator for their own room, not by a vendor.
>
> Demo, no sign-up: https://threadcount.tech/demo
---
## 3. Reply templates
**"What does it cost?"**
> The software is free — run it on your own server and there is nothing to pay. Hosted on
> threadcount.tech it is free for a room under 60 staff records, and $1,290 a year for a facility
> past that, which covers the hosting, 35 days of backups and me answering email the next business
> day. No per-device or per-user charge, and every feature is in every edition. Health services
> with several sites: $4,990 a year for five, with one sign-in across them.
**"Who else uses it?"**
> Honestly — it's early, and I'd rather say that than pad a list. It runs a real linen room every
> day, which is mine. That's the trade: you'd be early enough to shape it, and I'm around to fix
> things quickly.
**"What about our data / IT security?"**
> Fair question and the right one to ask before real staff data goes in. Everything is Australian
> hosted, kept separate per facility, and only visible to the accounts you invite. There's a plain
> summary at https://threadcount.tech/security and a full privacy policy at
> https://threadcount.tech/privacy — worth sending to your information security team early rather
> than late.
**"Can we get our data out?"**
> Yes, whenever you want. Every report and register exports as CSV, the ward request queue included.
> An admin can also download the whole facility as a single backup file — catalogue, staff,
> departments, suppliers, locations, and every issue, order and count. There's nothing to cancel and
> nothing held back.
---
## 4. Fifteen-minute demo script
Do not tour the menus. Walk one morning at the counter.
1. **Set the scene (1 min).** "This is a linen room with a catalogue, a staff register and stock on
the shelf. I'll do what a coordinator does before lunch."
2. **Issue to someone (3 min).** Scan a garment, pick the staff member. Point out the ward, cost
centre and last-issued sizes already on the record, and the entitlement balance on screen. Issue
below entitlement so the credit slip appears.
3. **The exception (2 min).** Try to go over entitlement. Show that it isn't blocked — it asks for
the approval, and records who approved it. This is the moment most coordinators lean in.
4. **A size change (1 min).** Exchange a medium for a large as one movement. Note that this is the
thing that quietly wrecks per-size numbers when it's done informally.
5. **Count a shelf (3 min).** Open a stocktake on the phone, scan a few, show the expected figure
on screen, force a variance and let it ask for a reason.
6. **What finance gets (3 min).** The cost centre report, one line per ward, and the CSV. Click a
ward's code to open the issues behind its figure — that is the answer to the question the whole
call opened with. Say plainly that if their finance team needs a different layout, that's a small
change.
7. **Close (2 min).** "There's a 60-day trial with no card. The demo resets, so if you want to try
it properly, set up your own facility — the catalogue and staff register come in from CSV."
**If time is short**, cut steps 4 and 6. Never cut step 3; the approval flow is what distinguishes
this from a spreadsheet.
---
## 5. Boilerplate
Reuse these verbatim so the description is identical everywhere it appears. Consistency across
listings is worth more than clever variation.
**One line (≤ 90 chars)**
> Uniform stock management for hospitals, aged care and clinics. Free to run, hosted from $1,290 a year.
**Short (≤ 160 chars — matches the site's meta description)**
> Uniform stock management for hospitals, aged care and clinics. Track what's on the shelf, who
> took it and what it cost the ward.
**Medium (~60 words)**
> ThreadCount is uniform stock management for hospitals, aged care and clinics. It follows a garment from
> the shelf, to the person wearing it, to the supplier order that puts another one back — issuing
> against entitlement with manager approvals, barcode scanning from a phone, stocktakes by shelf,
> and cost centre reporting finance will accept. Built by a uniform coordinator for their own room.
**Long (~120 words)**
> ThreadCount is uniform stock management for hospitals, aged care and clinics. The software is free
> and the code is published; hosting is free for a small room and $1,290 a year for a facility, with
> no per-user charge and every feature in every edition.
>
> It answers the two questions a linen room can rarely answer: what is on the shelf in each size,
> and who took it. Garments are issued to a named person against their entitlement, with their
> manager's approval recorded where someone goes over. Barcodes are read by the camera on an Android phone,
> so a room with a phone in it buys no scanner hardware at all. Stocktakes are counted by shelf with
> variance reasons captured while the reason is still known. Uniform spend attributes to the ward
> that incurred it and exports as a journal finance can upload.
>
> Australian hosted, separate per facility, and yours to take away — every report and register
> exports as CSV, and the whole facility as one backup file.
**Category words for listing forms:** inventory management, healthcare, uniform management, asset
tracking, stocktake, barcode scanning, hospital operations.
**Facts that must stay accurate everywhere:** free to run yourself; hosted free under 60 staff
records, $1,290 a year per facility above it, $4,990 a year for a health service of five (prices in
`lib/plan.ts`); facilities from before plans stay free with everything; Australian hosted; web plus an Android app; no
iOS app; camera scanning works in the Android app and in Chrome or Edge, and nowhere else — an
iPhone types the code or uses a USB scanner; every report and register exports as CSV, the ward
request queue included — it was the last one without one; the whole facility exports as one backup
file; there is no sales team.
+66
View File
@@ -0,0 +1,66 @@
# The notice to existing rooms
The pricing page promised: *"If that ever changes, the rooms using it will hear before the website
does."* This is how they hear. It goes to every active administrator of every facility that exists
today, sixty days before the plans switch is turned on, and the website does not change until that
day. `scripts/send-plans-notice.cjs` sends it from the production box, and refuses to send without
`--send` — run it once without to see who would get it.
The sixty days are the notice period the new Terms promise for price changes, so the first change
is held to the same standard as every later one.
**Facts the email states, which must be true on the day:**
- The facility is grandfathered: hosted free, with everything, for as long as it exists.
- Nothing needs doing, and nothing on their screens changes.
- New facilities from the go-live date start on Hosted Small (free under 60 staff records) or a
Hosted Facility trial ($1,290 a year after it). Prices from `lib/plan.ts`.
- The source will be published so anyone may run it themselves.
- Replies go to hello@threadcount.tech.
## The email
**Subject:** ThreadCount is introducing plans on {DATE}. Yours stays free.
> Hello {FIRST},
>
> A note from the person who runs ThreadCount, sixty days ahead, because the pricing page said you
> would hear before the website did.
>
> On {DATE} ThreadCount will start charging new facilities for hosting. Nothing changes for
> {FACILITY}. You signed up while it was free, and it stays free for you — every feature, every
> report, both apps, no ceiling on staff records — for as long as your facility exists. There is
> nothing to do, nothing to sign, and nothing on your screens will look different on the day.
>
> What is changing, for facilities created from {DATE}:
>
> - The software itself stays free. The code will be published so anyone can run it on their own
> server, with everything in it.
> - Hosting on threadcount.tech stays free for a room under 60 staff records.
> - A larger facility hosted on threadcount.tech will pay $1,290 a year (or $129 a month), which
> covers the servers, 35 days of backups, and a person who answers email the next business day.
> - Health services running several facilities will be able to buy them together, with one sign-in
> across sites.
>
> The Terms of Service now carry a Fees section that writes the grandfathering down, so it does not
> depend on a promise in an email: https://threadcount.tech/terms
>
> Why now: running it for other people costs money and time, and I would rather charge new rooms
> plainly than let the thing quietly stop being maintained. Charging the rooms that trusted it
> first was never on the table.
>
> If you would prefer to pay anyway, or your health service wants the multi-site arrangement, reply
> to this email. Otherwise, carry on exactly as you are.
>
> Kyle
> ThreadCount · hello@threadcount.tech
## After sending
- The script appends "Plans notice sent {today}, go-live {DATE}" to each facility's plan note, so
the operations console shows who was told and when.
- Put the go-live date in the calendar. On the day: turn the plans switch on in the operations
console (Switches Plans). The pricing page, the home page, the FAQ, the footer and the sign-up
choice all change within a minute; nothing needs deploying.
- Update the outreach pack's date placeholders, if any remain, and the Play listings' long
description if it says "free" without qualification.
+108
View File
@@ -0,0 +1,108 @@
# Self-hosting ThreadCount (Community edition)
The Community edition is the hosted product with `EDITION=community` in its environment. This
page says what that changes, how to run it, how to keep it, and what is not included.
## What `EDITION=community` changes
| | Hosted (threadcount.tech) | Community |
|---|---|---|
| Features | All | All |
| Plans, staff ceiling, read-only for non-payment | Yes | None. Every facility behaves as a grandfathered one. |
| Public website at `/` | The marketing site | Redirects to sign-in |
| Demo facility | Yes | Off |
| Operations console (`ops.` hostname) | Yes | Off unless `OPS_SESSION_SECRET` and the ops database roles are set up |
| Cloudflare Turnstile on sign-in and sign-up | Required | Optional. Set both keys to enforce it; without it the per-address rate limits stand alone. |
| Error reports | To ThreadCount's own GlitchTip, scrubbed | Nowhere, unless you set `NEXT_PUBLIC_GLITCHTIP_DSN` to your own |
| Usage statistics | To ThreadCount's own Umami, scrubbed | Nowhere. The tracker only mounts on `*.threadcount.tech`. |
| Single sign-on for a facility | Via ThreadCount's Jackson broker | Run your own [BoxyHQ Jackson](https://boxyhq.com/docs/jackson/overview) and set `JACKSON_URL` and `JACKSON_API_KEY`; otherwise no SSO |
| Transactional email | ThreadCount's SMTP | Your SMTP, or none |
Everything the legal pages on threadcount.tech say applies to the hosted service. Your instance
is yours: publish your own privacy statement for your staff. The pages under `/privacy`,
`/terms` and so on still render on a Community instance, but they describe threadcount.tech.
## Running it
`docker-compose.yml` runs three services: `db` (Postgres 16, in a named volume), `migrate` (a
one-shot `prisma migrate deploy` that runs before the app on every `up`), and `app` (Next.js's
standalone server, port 3000, photographs in a second named volume). Put TLS in front of port
3000. A minimal Caddyfile:
```
uniforms.example.health {
reverse_proxy 127.0.0.1:3000
}
```
The image is built on your machine, not pulled, because `NEXT_PUBLIC_SITE_URL` and the optional
Turnstile site key are compiled into the browser bundle. If you change either, rebuild:
`docker compose up -d --build`.
### The first facility
Open the hostname. Create an account; that creates a facility and makes you its administrator.
Under Settings, name your staff groups first (nothing works until a facility has them), then load
the catalogue, departments, staff register, barcodes and opening stock from CSV under Settings
Data. Add a second administrator under Settings Account Users before you sign out — a
forgotten password with one admin and no SMTP locks the facility.
Then set `SIGNUPS_DISABLED=1` in `.env` and `docker compose up -d`. Your instance is now one
facility, and nobody else can create another.
## Upgrading
```sh
git pull
docker compose up -d --build
```
`migrate` applies any new schema migrations before the new app starts; the old app keeps
serving until then. Take a backup first (below). Releases that need more than that say so in
their notes.
## Backups
`docker/backup.sh <directory>` writes one directory per run: the database as a custom-format
`pg_dump` and the photographs as a tarball, keeping the last fourteen. Run it nightly from cron:
```
30 3 * * * cd /srv/threadcount && docker/backup.sh /srv/backups/threadcount >> /var/log/threadcount-backup.log 2>&1
```
Copy that directory somewhere off the machine. A backup on the same disk as the database is not a
backup.
Separately, an administrator can download the whole facility as one JSON file from Settings
Data at any time, and restore it into a fresh instance from the same screen. That file is the
portable form; the `pg_dump` is the fast one.
### Restoring
```sh
docker compose down
docker volume rm threadcount_db threadcount_photos # only if you mean to replace everything
docker compose up -d db
docker compose exec -T db pg_restore -U threadcount -d threadcount --clean --if-exists < 2026-09-13-033001/threadcount.dump
docker compose up -d
docker compose cp 2026-09-13-033001/photos app:/data/ # after extracting photos.tgz
```
## The Android apps
The two Play apps (ThreadCount, the counter; ThreadCount Staff) load their screens from
`https://threadcount.tech` and cannot be pointed at another host. On a self-hosted instance use
the same screens in a phone's browser: `https://your-host/m` for the counter (camera scanning
works in Chrome and Edge) and `https://your-host/my` for staff. Add either to the home screen.
## Environment reference
`.env.example` lists every variable the application reads, with a comment on each. The ones that
matter for a Community instance are `SESSION_SECRET`, `POSTGRES_PASSWORD`, `NEXT_PUBLIC_SITE_URL`,
`EDITION`, the `SMTP_*` group, and `SIGNUPS_DISABLED`. Leave the operations console, Cloudflare
Access, Listmonk, Umami and GlitchTip variables empty unless you run those yourself.
## Getting help
Open an issue on the repository. If you would rather someone else ran it, that is what
[threadcount.tech/pricing](https://threadcount.tech/pricing) is for.