Repository navigation
Releases: kevincardwell/galley
Release list
Galley v1.3.0
The website itself is now part of the project: connect a workspace to the site's git repository and everyone on it can change the site in a live preview, then send the changes to GitHub as a pull request. Anything that needs a hand on your side is under Upgrading.
Added
- The Site tab: edit the website itself. Connect a project to its git repository, when you create it or
later, and everyone on the project gets a live preview of the site. Click text in the preview to change
it, or open the code editor beside it. Send for review commits as the sender (with Co-authored-by lines
for everyone else who edited), pushes agalleybranch and opens or updates a pull request, and the
toolbar shows the GitHub Actions build for it. After the merge, N new on main brings the working copy
up to date and starts the next round. - Commit email on your account page, for when your sign-in address should not appear on public commits.
Security
- The preview is served with
Content-Security-Policy: sandbox, so the site's own scripts run in an opaque
origin and cannot use the viewer's Galley session. Members cannot read or write.git/or.github/, so
editing a file can neither run code on the server nor change the workflows that hold the repository's
secrets. Repository addresses are https only, and the token never touches.git/configor a command line.
Upgrading
- The image now includes
git. Migration0013adds thesitestable andusers.git_email. - Backups include
sites/(the working copies, which hold edits nobody has sent yet).
Galley v1.2.0
The client's side of the share link, rebuilt around getting the words signed off: clients can now suggest
their own wording instead of describing it in a comment, and the page tells them what is waiting on them.
Anything that needs a hand on your side is under Upgrading.
Added
- Clients can suggest wording. Every section on the share page has "Suggest wording": the client edits the
words, sees their changes highlighted as they type, and sends it with an optional reason. In the editor it
arrives as a comment showing only the paragraphs they changed, with Accept and Dismiss. Accepting
puts their words into the live section — anyone editing sees it at once — keeps its headings and lists, and
leaves the previous wording in the version history. - The share page says what is waiting on the client. A list at the top links to every section in review
that they have not approved yet, the page tabs carry a count, and a page with more than one such section
has Approve all. It is the same list the weekly digest sends, so the email and the page agree.
Changed
- New share links let the client comment, suggest and approve from the start. That is what the link is
for; the box is still there to untick. Existing links keep whatever they had. - The client gives their name once, in the page header, instead of in a form under every section. The
per-section form is now three buttons — Approve, Suggest wording, Comment — and draft sections no longer
offer Approve. - Sections a client writes keep their headings and lists when the paragraph count is unchanged; only
the paragraphs they reworded lose bold, italic and links.
Fixed
- The details pane showed 0 characters until the section's editor had reported in.
- Replacing words that had never been saved lost them. Sample-project sections, and anything else with no
version history yet, now get their current wording recorded as a version before a restore, an accepted
suggestion or a client's write replaces it. - On a phone, the share page cut the project and client names to a few letters.
Upgrading
- Migration
0012adds two columns tocomments. It runs on start, after the usual pre-migration backup.
Galley v1.1.0
Two-factor sign-in and invite links with a chosen lifetime, plus the fixes from a review pass
over sign-in, sharing, backups and the interface. Anything that needs a hand on your side is under
Upgrading.
Added
- Two-factor authentication. Account → Two-factor turns on a six-digit code from any authenticator app
(Google Authenticator, 1Password, Aegis). Scan the QR or type the key, confirm one code, and ten one-shot
recovery codes are shown — the only time they are shown. Sign-in then asks for a code after the password,
including when a reverse proxy signed the person in. A code cannot be used twice, and the code step is
throttled the same way the password step is. Any account can use it; admins are who it is for. - Invite links last as long as you say. The invite form now has "Link expires": after 1, 7, 30 or 90 days,
or never. Seven days stays the default, so nothing changes unless you pick something else, and the invite
email names the date rather than counting days from whenever it was sent.
Fixed
- Two-factor codes could be spent twice by two requests arriving together, and a simultaneous pair of
recovery codes could undo each other's removal — a spent code came back. The check and the write are now
one guarded update, so exactly one request can spend a code. - The code step's rate limit was reset by signing in again. With the password known, an attacker could
sign in, try seven codes, sign in again, and repeat for as long as they liked. The second factor now has
its own counter that only a correct code clears, and a successful password no longer wipes the address-wide
counter either. - Login throttling could be evaded with a forged header.
TRUSTED_PROXY_HOPS=0, documented for setups
with no proxy, fell back to the lastX-Forwarded-Forentry — precisely the one a client can set. Zero now
ignores forwarded headers, a chain shorter than the configured depth is ignored too, and the compose file
binds the port to127.0.0.1so a default install is not reachable directly. - A collaborator's tab left in the background silently stopped receiving edits. The sweep that clears
clients whose stream close went unobserved now probes the stream first: a live-but-quiet tab keeps its
place, and only a genuinely dead connection is dropped. - The workspace favicon fetch could be pointed at the local network with DNS rebinding. The address was
checked and then resolved again by the fetch. It is now fetched from the address that was checked, the
IPv6 private/loopback ranges the check missed (link-local, unique-local, multicast, NAT64 and
IPv4-compatible forms) are covered, and a name that resolves to any private address is refused. - Signed-in users who were not managers could read a project's share token out of the page data and
hand out a link that lets guests comment, approve and upload. The token is now only sent to members who
can manage the share link. - Enabling or disabling two-factor did not sign out other sessions, so a session opened with a possibly
leaked password survived the exact change made to contain it. Both now sign out every session but the one
making the change, as changing a password already did. - An archived project still served its share link, its files and its calendar feed to anyone holding the
old URL. Archiving now closes those for reading as well as writing; unarchiving brings the same links back. - Forward-auth had no way to prove a request came from the proxy. With
GALLEY_AUTH_HEADERSon, any
request that could reach the port with aRemote-Emailheader signed in as that person.GALLEY_AUTH_SECRET
now makes the proxy sendx-galley-auth, checked in constant time, and without it the first (admin) account
can no longer be created through the proxy — so an accidentally exposed port is not a way to claim a fresh
instance. Galley logs a warning at startup when headers are on with no secret. GALLEY_URLandMAX_UPLOAD_MBsilently stopped working after the settings page had been saved once.
The stored copy shadowed the environment variable, so changing it in compose or the Unraid template did
nothing on restart. The environment now wins over the stored value, always.- A nightly backup that failed was invisible. Failures only reached the container log, and a container
that was down when the chosen hour passed skipped that day entirely. The schedule now catches up after the
hour, retries failed runs at a slower cadence, and Admin → Backups shows when the last attempt ran and
whether it worked. Backup zips are written under a.partname and renamed when complete, so a crash
cannot leave a truncated file that looks restorable, and the zips and pre-migration snapshots are created
0600— they hold the mail password and every session token. - A local
npm run buildpackaged the working copy'sdata/directory (database, WAL and backup zips)
into.next/standalone. The published image was never affected, but a locally built one could have been.
The build now removes it, and the route traces exclude it. - The container healthcheck called a full or read-only data volume healthy, because it only proved the
database answered. It now also checks the data directory is writable with room to spare. - The entrypoint only checked the top-level owner, so a hand-restored backup or an interrupted chown that
left some files root-owned produced permission errors with no explanation. It now probes the database and
uploads directory too, and a read-only/datagets a plain message instead of amkdirtrace and a crash
loop. - A pre-migration snapshot could collide with an earlier one on a crash-looping boot and abort the
upgrade, and generated SQLite table rebuilds relied on a pragma that does nothing inside a migration
transaction. Both fixed. - The mobile navigation drawer kept its links in the tab order while closed, trapped no focus when open,
and the contrast palette failed WCAG AA in both themes (secondary text, status colours, and the dark-theme
accent). The drawer is nowinertuntil opened and moves focus in and back out; the palette meets AA. - The command palette was a plain
div: no dialog semantics, no focus trap, and arrow-key results were
invisible to screen readers. It is now a native modal dialog with combobox/listbox semantics. - Toasts vanished after 2.2 seconds with no way to keep or dismiss them, including errors. They now last
longer, pause while hovered or focused, and can be dismissed. - Cmd+S could report "Saved" over edits that had not reached the server. The flush now says whether the
bytes landed; a failed flush keeps the dirty state and says so. - Smaller ones: upload progress no longer floods the live region with every percent; each main screen has
a skeleton that matches the layout it is about to show instead of the workspace grid; tab rows are links
witharia-currentrather than pretending to be ARIA tabs; icon buttons are 40px on touch screens; the
lightbox no longer re-adds its key listener on every render;?no longer opens the shortcut sheet over
the task inspector; role, member and calendar actions say so when they fail; the theme toggle updates the
installed-app chrome colour;/api/searchresponses are marked private; and the release workflow now runs
the type, lint and test gates before publishing:latest.
Changed
- Fonts are self-hosted. Instrument Sans and Newsreader were loaded from Google's CDN, which needs the
CSP to allow that origin and sends every visitor's IP to Google.next/fontdownloads and serves them
from the app itself, so the CSP no longer allows any third-party style or font source.
Upgrading
If you reach Galley directly rather than through a reverse proxy, publish the port as 3000:3000 and set
TRUSTED_PROXY_HOPS=0, or copy the new compose.yml; the default now binds to localhost. Behind a proxy,
nothing changes. Otherwise nothing to do: the migration adds the two-factor columns and makes an invite's
expiry nullable, and existing invites keep the expiry they already had.
Galley v1.0.1
A patch release, mostly for one bug that quietly ate .jpeg uploads.
Fixed
A .jpeg upload was stored where nothing would look for it. The file was written under the extension it arrived with, while everything that reads a file back derives the path from the stored MIME type — and .jpg and .jpeg share one MIME type. So a .jpeg landed on disk as original.jpeg and was looked for as original.jpg. The upload reported success, the recorded byte count matched the file exactly, and the asset then said "The original file is missing". It was not missing; it was six characters away.
Uploads now use the canonical extension, so there is one rule rather than two. Files already stored the other way are repaired automatically — renamed into place the first time anything asks for them, including the zip export, which had been dropping them without a word. An asset that failed only for this reason is retried on the next start instead of staying failed for good. Nothing needs re-uploading.
An image that failed to process showed a broken image. The grid assumed every image had a thumbnail whether or not processing had succeeded, so the browser's broken-image glyph appeared instead of the file icon and the "Could not process" note. Videos already checked this; images do now too.
Changed
Light is the default theme. It used to follow the operating system before anyone had chosen anything, so opening Galley on a dark desktop gave you a dark Galley you never asked for. Dark is now something you pick — directly, or by picking "follow the system", which still works. Existing choices are untouched.
The Docker Hub blurb is the current tagline rather than the one from before the repositioning.
Verified
Every accepted type was put through end to end afterwards — jpg, jpeg, png, webp, gif, avif, svg, mp4, webm, mov and pdf — checking where the file lands, what is derived from it, dimensions, duration, palette, and that both variants serve with the right content type. SVGs keep an SVG preview, videos get poster frames, PDFs correctly get neither.
Upgrading
Pull and restart. No migrations. Unraid users: Docker tab → Check for Updates.
ghcr.io/kevincardwell/galley:1.0.1 and kevincardwell/galley:1.0.1, linux/amd64 and linux/arm64.
Full notes in CHANGELOG.md.
Galley v1.0.0
The first release meant for other people to run.
A galley proof is the first typeset pull of a page, laid out so the words can be checked before anything goes to print. Galley does that for a website: the copy is written page by page and section by section, it moves from draft to in review to approved, and the client signs it off through a link with no account and nothing to install.
The client's side is not read-only any more
Website projects rarely stall on the build. They stall waiting for the words, the logo and a decision. So through one link a client can now:
- Comment and approve, section by section — and an approval lapses automatically if the wording changes afterwards, so it always refers to the words they actually agreed to
- Send files in — their logo, photographs, the old brochure — landing in the project's library labelled with who sent them
- Write the sections you hand over, arriving in the version history like any other edit
- Get a weekly note: what you changed, what is still waiting on them, and the link back
Also new
- Project templates — start from an existing project and keep the task list and page structure, without the last client's content
- Repeating tasks — weekly to yearly, so a care plan lives here alongside a build
- Read what was emailed — Admin → Email lists recent deliveries and a row opens the message itself
Fixed, including two things that were quietly lying
- Restoring an old version of a section did nothing. It had never worked since collaborative editing shipped
- A client's approval never lapsed when the copy was rewritten underneath it
- An editor was shown a button only a manager could use, and it threw when clicked
- The instance-wide supplier directory could be edited by anyone signed in, including a viewer on one project
- A failed database migration leaked a connection per request instead of failing the boot
- The README described a backup method that silently loses recent writes
Installing
mkdir galley && cd galley
curl -O https://raw.githubusercontent.com/kevincardwell/galley/main/compose.yml
docker compose up -dghcr.io/kevincardwell/galley:1.0.0 and kevincardwell/galley:1.0.0, linux/amd64 and linux/arm64. There is an Unraid template in Community Applications.
The container now sets up its own data directory whatever the host owns it as — Unraid's nobody:users, a root-owned bind mount, or a named volume — then drops to PUID:PGID and never runs the application as root.
Upgrading from v0.1.0 needs nothing: six migrations apply on start, and the database is copied to backups/pre-migration-<date>.db first.
Full notes in CHANGELOG.md.
Galley 0.1.0
Galley is a self-hosted home for the work around a project: what needs doing, what it will say, who you are hiring, when it happens, and the files that go with it. One container, one SQLite file to back up, no seats to buy.
Getting it running
mkdir galley && cd galley
curl -O https://raw.githubusercontent.com/kevincardwell/galley/main/compose.yml
docker compose up -dOpen the app, create the admin account, and tick the box to load a worked example so nothing starts empty.
| Registry | Image |
|---|---|
| GitHub | ghcr.io/kevincardwell/galley:0.1.0 |
| Docker Hub | kevincardwell/galley:0.1.0 |
Both are built for linux/amd64 and linux/arm64.
What is in this first release
- Projects with their own accent colour, client, status and favicon, each showing where the work has got to.
- Tasks in sections you name, as a list or a board, with drag to reorder, due dates, assignees, checklists, comments and attachments.
- Copy as pages of sections with draft, in review and approved states. Several people can write in the same section at once and see each other's cursors. Every save is kept, with a word-level diff and one-click restore, and copies out as clean Markdown or HTML.
- Calendar in month, week and agenda views, showing task deadlines alongside scheduled entries, with drag to reschedule and a subscribable feed.
- Suppliers, a shared directory of the printers, photographers and freelancers you use, linked into the projects that book them with status, cost and notes.
- Files with thumbnails, dimensions, video posters, colour palettes, tags and folders, linked to the task or paragraph they belong to.
- A client link that is read-only by default, or lets the client comment on and approve individual sections without an account.
- Invite-only access. No public sign-up. People see only the projects they are added to.
- Email through your own SMTP server or through Resend, Postmark, SendGrid or Mailgun, so invites send themselves even where SMTP ports are blocked.
- Notifications for mentions, assignments and client feedback, in the app and by email.
- Backups on demand or nightly, with retention, from the admin panel.
- Installable on a phone from Safari or Chrome, opening full screen with its own icon.
- Search across everything you can see, workspace export as a zip, light and dark themes, and a keyboard shortcut for most things.
Before you expose it to the internet
- Serve it over https and set
GALLEY_URL. Home-screen installation needs it, and session cookies are marked secure in production unless you explicitly set anhttp://URL. - Set
TRUSTED_PROXY_HOPSto the number of reverse proxies in front, so login rate limiting sees the real client address. - Treat your backups as secrets. The database holds your mail password or API key in plain text, along with password hashes and share links.
Notes
This is a first release: the database schema is still likely to change between versions, so read the release notes before upgrading. Migrations run automatically on start.
Built and reviewed with a security audit covering authentication, authorisation, file handling and injection before publication. Reports of anything missed are welcome.