Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
브라우저 사진 부스로 Phomemo 블루투스 열전사 프린터에 스티커를 출력합니다
Browser photo booth that prints stickers on a Phomemo Bluetooth thermal printer — live looks, photo strips, QR sharing, self-hosted. PWA, no app needed.
라이프스타일TypeScript · HTMLGitHub @marijnvandevoorde온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Turn any laptop, tablet or Mac mini with a webcam into a party photo booth that prints black-and-white stickers on a cheap Bluetooth thermal printer. Open-source, runs in the browser (PWA), self-hosted, no app to install.
Guests strike a pose, watch the countdown, and walk away with a sticker — plus a colour copy on their phone via a QR code. Great for weddings, birthdays, company parties, school fairs and festivals.
Try it now: boot.small-victories.co — open it in Chrome (Android, Mac, Windows, ChromeOS) or in Bluefy on iPad / iPhone. “Start without printer” works on anything with a camera.
Templates: plain, a typed title / names / date / hashtag, or your own header and footer images.
Unattended mode: auto-print, copies per print, a print limit per photo, an idle “tap to take your photo” screen that cycles recent photos, and friendly error screens that recover by themselves when the camera or printer drops out.
Paper meter: set the roll length and the booth warns you (not the guests) when it's running low.
Counters: sessions, prints, shares and the busiest hour, per event.
Every photo kept: a colour copy of every session in the browser, ready to download as a ZIP the next morning.
Saved setups: save, export and import whole setups (settings and template images) as a file.
Event galleries (paid): a host signs up on /event.html and pays once
with Stripe — or, in the iOS app, with an in-app purchase right in
settings — and gets a setup link for every booth and a private gallery
with a ZIP of all the event's photos, kept for a year. The owner manages
events in /admin (admin token + authenticator code).
Guest-proof kiosk: once started, the booth stays on the camera — a reload or app restart comes straight back and reconnects to the same printer by itself (Chrome and the app). Leaving the booth needs the host PIN. No pull-to-refresh, zoom or long-press menus. For a fully locked tablet, use iPad Guided Access or Android app pinning.
Settings live on a hidden page, grouped for hosts (Tonight, Event, What
guests see, Printer & paper, Photos, Security, Advanced) with a short
explanation under every setting. To get there: long-press the “Photoboot”
title on the start screen, or long-press the top-left corner of the camera
view for 2 s for the host menu (settings, reconnect or change printer, stop
booth). A tap on the red “Reconnecting…” pill retries the printer at once. Both
ask for the PIN when one is set; /?admin=1 opens settings behind the same
PIN. Stop booth (host menu or Settings → Security) brings back the
start screen.
/test.html prints
calibration rulers../run.sh # downloads a portable Node into ./.node, then starts Vite
That serves the booth over HTTPS on your LAN (accept the self-signed
certificate) at https://<this machine>:5173. Camera and Bluetooth both need
HTTPS or localhost. Shared photos land in ./shares.
Other scripts: ./run.sh build builds into dist/; ./print.sh opens a page
to print a single photo from this computer.
TypeScript (strict) with no framework, built by Vite, linted and formatted
by Biome, tested with Vitest. The server runs its .ts files directly on
Node 24 — no build step.
npm ci
npm run dev # Vite dev server with HTTPS + the share/event API
npm run check # typecheck + lint + unit/server tests
npm run test:e2e # builds, then drives headless Chrome with a fake camera
# and a fake Bluetooth printer (needs Google Chrome)
npm run format # Biome: format + safe fixes
Tests need Node 22+. Running the booth itself only needs Node 20.19+
(./run.sh picks the right Node for older Macs).
The production server is plain Node (no dependencies) serving dist/ plus
the share and event routes. With Docker:
docker compose up -d --build
docker-compose.yml is set up for a Traefik reverse proxy; adapt the labels,
domain and volumes to your host.
Production (boot.small-victories.co) runs on the Kubernetes cluster sv-oke.
npm run deploy ships origin/main there with the shared sv-deploy script from
the Oracle repo (config in deploy.env; npm run deploy:check shows what runs;
SV_DEPLOY overrides the script's path, default ../Oracle/deploy/sv-deploy).
| Variable | Default | What it does |
|---|---|---|
BASE_URL | from request | Public origin used in QR links, e.g. https://booth.example.com |
PORT | 8080 | HTTP port |
SHARE_DIR | ./shares | Where shared photos are stored (without S3_BUCKET) |
EVENTS_DIR | ./events | Where server events are stored |
DATABASE_URL | — | MySQL instead of SQLite: mysql://user:pass@host:3306/db?ssl-mode=REQUIRED |
S3_BUCKET | — | Keep photos in this S3-compatible bucket instead of SHARE_DIR; with S3_ENDPOINT, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE |
SHARE_TTL_DAYS | 30 | Shared photos are deleted after this many days (0 = never) |
SHARE_MAX_MB | 5000 | Refuse uploads once the photo folder is this big |
ADMIN_TOKEN | — | Enables /admin (log in with it + an authenticator code) |
ADMIN_TOTP_RESET | — | 1 forgets the enrolled authenticator (next login sets up a new one) |
PUBLIC_URL | BASE_URL | Origin used in emailed setup / gallery links |
PAID_RETENTION_DAYS | 365 | How long a paid event keeps its photos |
EVENT_PRICE_CENTS | 1900 | Web price of an event gallery, in cents |
EVENT_CURRENCY | eur | Currency of that price |
APPLE_BUNDLE_ID | co.smallvictories.photoboot | iOS app whose in-app purchases are accepted |
All settings live in a .env next to docker-compose.yml: copy
.env.example, which lists every variable with its default, and
fill in what you need (empty = default). Compose loads it into the container,
and npm start / npm run dev read it too.
Hosts pay for an event gallery on /event.html through Stripe Checkout;
the server talks to Stripe's API directly (no SDK).
STRIPE_SECRET_KEY.https://<host>/api/stripe/webhook
with the events checkout.session.completed,
checkout.session.async_payment_succeeded and charge.refunded. Copy
its signing secret into STRIPE_WEBHOOK_SECRET.STRIPE_AUTOMATIC_TAX=1. Checkout always offers an invoice.Test mode works the same: use the sk_test_… key and a test-mode webhook
(or stripe listen --forward-to localhost:8080/api/stripe/webhook). A full
refund in Stripe ends the event's paid perks; its photos stay.
Inside the iOS app, an event gallery is sold through Apple in-app purchase
(App Store guideline 3.1.1); the web keeps its own checkout. The app asks the
server for a pending event, buys the consumable with that event's
appAccountToken, and sends the signed StoreKit 2 transaction to
/api/apple/redeem. The server verifies Apple's signature and certificate
chain itself (pinned Apple Root CA - G3, server/apple-root-ca-g3.pem) and
activates the event; the host gets the usual email and the booth sets itself
up right away. To set it up:
co.smallvictories.photoboot.eventgallery
(or set APPLE_PRODUCT_ID on the server and VITE_APPLE_PRODUCT_ID when
building the app), a price, a display name and description, and a review
screenshot of the settings card. Submit it together with the next app
version.https://<host>/api/apple/notifications as both the Production and the
Sandbox URL. Refunds then end the paid perks, and a purchase whose app
never got back to the server still activates its event.APPLE_ALLOW_SANDBOX=0.Without a server event, only photos a guest chooses to share leave the
booth; a booth in an event uploads every photo to that event's gallery
(except the ones a guest retakes). Each
uploaded photo gets an unguessable link and a page where the guest can save it or delete it, and
it's deleted automatically after SHARE_TTL_DAYS. Images are served with
Cache-Control: private, so a CDN doesn't keep copies after a delete.
Uploads are rate-limited per IP unless they come from a booth with a valid
event key. Everything else — settings, counters, the full photo archive —
stays in the booth's browser. An hourly job (server/retention.ts) also
deletes unpaid and expired events and payment records older than the
accounting period. The GDPR record of processing is in docs/gdpr.md; hosts
get a printable notice for their guests at /sign.
Auto-detect recognises a printer by its Bluetooth name when you tap Connect printer, and sets the paper width, resolution and tear margin for it. Not recognised? Pick your model under Settings → Printer & paper. You can override the width and margin there too.
✅ printed on real hardware · 🧪 implemented from the protocol as documented by other open-source projects, not tried yet. Got one of the 🧪 printers? Please tell us how it went: open a printer report, whether it worked or not.
| Printer | Also sold as / Bluetooth name | Width | Status |
|---|---|---|---|
| Phomemo P2, P2S | P2… | 552 of 576 dots, 300 dpi | ✅ |
| Phomemo M02S, M02 Pro | M02S, M02PRO | 552 / 600 dots, 300 dpi | 🧪 |
| Phomemo M02, M02X, T02, M03, Memoking | M02…, T02…, Mr.in… | 384 (M03: 432) dots, 203 dpi | 🧪 |
| Phomemo M110, M120, M220, M221 (labels) | M110, Q199E, Q0xx… | 320 / 560 dots, 203 dpi | 🧪 |
| Cat printers GB01–GB05, GT01, X5–X8, LY0x | iPrint / Tiny Print apps, AliExpress, Temu, Amazon | 384 dots, 203 dpi | 🧪 |
| Cat printers MX05–MX13, YT01, PD01 | iPrint / Fun Print | 384 dots | 🧪 |
| MXW01, Flying Tiger, StickerMate, C17 | Fun Print / WalkPrint | 384 dots | 🧪 |
| LuckJingle / Luck P D1, L1, S1 | LuckP_…, DP_… | 384 dots | 🧪 |
| Crafts & Co 3128 (Action) | C&Co… | 384 dots | 🧪 |
| Fichero Mini Pocket Printer (Action) | pick it in settings: the picker then lists every device | 384 dots | 🧪 |
| Lidl / Silvercrest Mini Pocket Printer | Mini Pocket Printer… | 384 dots | 🧪 |
| PeriPage A6, A6+ | PeriPage… | 384 / 576 dots | 🧪 |
| Marklife P50 | P50… | 384 dots | 🧪 |
| Paperang P1, P2 | Paperang… | 384 / 576 dots | 🧪 |
| Niimbot B1, B21 (labels) | B1-…, B21… | 384 dots | 🧪 |
| 58 / 80 mm BLE receipt printers (PT-210, MTP-II, MPT-III…) | any name: pick Receipt printer in settings | 384 / 576 dots |
Not supported:
The drivers build on the reverse-engineering work of NaitLee/Cat-Printer, rbaron/catprinter, jeremy46231/MXW01-catprinter, MultiMote/niimbluelib, vivier/phomemo-tools, 0xMH/fichero-printer, SwapnanilDhol/minixprint, Yrr0r/paperang-web, bitbank2/Thermal_Printer and opentlp.
Have a printer that isn't on the list, or a 🧪 one that doesn't print right? Open a printer report with its Bluetooth name and the app it came with: that's usually enough to tell which protocol it speaks.
Want to add it yourself? Pull requests are very welcome:
src/printers/models.ts with its Bluetooth name prefixes, resolution,
width and tear margin.PrinterBase in
src/printers/<family>.ts. Declare its Bluetooth channels and implement
init, printRaster and feed; the base class does the connecting,
timeouts, write pacing and reconnects. Register it in
src/printers/index.ts, and add a test with a fake printer like those in
test/printers.test.ts.verified: true on the model and move it
to ✅ in this README.Protocol notes are in CONTEXT.md.
TypeScript and Vite, no framework. Photos are dithered to 1-bit in
the browser (src/raster.js, src/effects.js), composed with the template
(src/strip.js) and sent to the printer in its own protocol over Web
Bluetooth (src/printers/). CONTEXT.md has the full map of the code, the
printer protocol and the decisions behind it.
MIT © Marijn Van de Voorde
APPLE_PRODUCT_ID | co.smallvictories.photoboot.eventgallery | The consumable that buys an event gallery |
APPLE_ALLOW_SANDBOX | 1 | Accept Sandbox purchases (TestFlight, sandbox testers); 0 = Production only |
BREVO_API_KEY | — | Sends the event emails through Brevo (without it they're logged) |
MAIL_FROM | [email protected] | Sender address (a verified Brevo sender) |
DB_PATH | EVENTS_DIR/photoboot.db | SQLite database (events, photos, payments, sessions), without DATABASE_URL |
UPLOAD_TOKEN | — | If set, uploads need this token (set it in settings) or an event key |
STRIPE_SECRET_KEY | — | Stripe secret key (sk_live_… / sk_test_…); without it there's no checkout |
STRIPE_WEBHOOK_SECRET | — | Signing secret (whsec_…) of the Stripe webhook endpoint |
STRIPE_AUTOMATIC_TAX | — | 1 lets Stripe Tax add VAT (the price is then VAT-inclusive) |
| 🧪 |