Files
projectsend/CONTRIBUTING.md
T
ignacionelson 745f24c7d9 Send users to the image and contributors to CONTRIBUTING
The README told everybody to clone the repository and run the development
stack, which builds nothing and installs nothing — so a user following it
got a crash-looping worker and a 500, and reported it as a bug (#1627).
They were reading the right document; it was pointing at the wrong stack.

Getting started now says what each path is for. Users fetch
compose.example.yaml and run the published image, which ships vendored and
pre-built and needs no toolchain — it has existed all along and was linked
from nowhere in the README. Contributors get one paragraph saying a clone
is a development copy, not an installation, and a pointer to
CONTRIBUTING.md, whose sequence was already correct.

CONTRIBUTING gains the sentence its readers were missing — why those steps
exist at all — plus the two things a contributor now meets: containers
that name the missing step instead of dying silently, and the fact that a
second checkout shares one Compose project name and will take over the
first one's containers. That last one cost me this working copy's stack
for a few minutes.

Also: DOCKER.md and INSTALL.md both linked README.md#development, an
anchor that has never existed. And UPDATE.md's build-from-source path told
operators to pull and rebuild with no dependency step, which is the same
trap on every release that moves composer.lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 02:17:20 -03:00

9.9 KiB

Contributing to ProjectSend

ProjectSend has been maintained since 2011 and is used by freelancers, agencies, NGOs, schools and government offices around the world. Thanks for helping keep it good.

Ways to contribute

  • Report a bug — open an issue with how you run ProjectSend (Docker or manual install), the version, and steps to reproduce.
  • Report a security vulnerability — do not open a public issue. Report it privately via GitHub security advisories.
  • Suggest a feature — start a discussion first. It saves everyone time to agree on the shape of a thing before code is written.
  • Translate — translations live in this repo as JSON files under lang/. Fixing or completing a locale is a pull request like any other.
  • Write code — read the rest of this document first.

Before you write code

For anything beyond a small fix, open an issue or discussion first. Large pull requests that arrive without prior discussion are hard to review and often need rework.

Setting up for development

A clone is a development copy, not an installation: vendor/ and public/build/ are deliberately not in git, so nothing runs until Composer and npm have filled them. That is what these steps do, and it is why the published Docker image — which ships both, already built — is what the README sends users to instead. From a fresh clone:

cp .env.example .env
docker compose up -d
docker compose exec app composer install
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
npm install && npm run build          # or `npm run dev` for hot reload

The app is then at http://localhost:8090. Until a staff user exists every request redirects to the first-run setup screen, which creates the initial administrator. For unattended provisioning you can instead set ADMIN_NAME / ADMIN_EMAIL / ADMIN_PASSWORD in .env before starting (the container creates the account on boot, idempotently), or run docker compose exec app php artisan projectsend:admin at any time.

A few things worth knowing:

  • Self-hosted installs run the community edition — PROJECTSEND_EDITION=community in .env.
  • APP_PORT / ADMINER_PORT / DB_PORT_FORWARD override the ports if they clash with something you already run.
  • Adminer, a database GUI, is available for development with docker compose --profile dev up -d adminer.
  • On later boots the container migrates automatically. The manual migrate above is only needed on the first install, before vendor/ exists.
  • Until composer install has run, the worker and scheduler containers have no application to run and exit with a message saying so; they pick themselves up once it has. If a page answers "ProjectSend is not installed yet" or "not built yet", it is naming the step that is missing.
  • Two checkouts of this repository share one Compose project name, so docker compose up in the second one takes over the first one's containers. Pass -p some-other-name when you want them side by side.

Staff and clients are different things. Staff — "system users" — administer the installation and upload files. Clients are the people files are shared with. There is no staff registration page: staff are created by an administrator. Client self-registration is optional and can require approval.

You don't need anything private to work on ProjectSend. The community-modules companion package is public and Composer fetches it for you; the paid Cloud package is not required by the core.

Before you open a pull request

Run the same checks CI runs, and make sure they pass:

docker compose exec app ./vendor/bin/pest        # tests
docker compose exec app ./vendor/bin/phpstan     # static analysis (level 8)
docker compose exec app ./vendor/bin/pint        # code style
npm run types                                    # TypeScript
npm run lint                                     # ESLint

If you touched the frontend, npm run build must also succeed.

Pull requests

  1. Fork the repo and branch off main.
  2. One logical change per pull request.
  3. Run the checks above and keep the existing code style.
  4. Write a clear description: what changes, why, and how you tested it.
  5. If your change affects the database schema, include the migration.
  6. If your change affects user-facing strings, use the existing translation helpers (t() / __()) — never hardcode English.

Licensing and the Contributor License Agreement

ProjectSend is free software licensed under the GNU General Public License v2, or (at your option) any later version. See LICENSE. That isn't changing.

Before your first pull request can be merged, you'll need to sign a Contributor License Agreement. A bot will comment on your pull request with a link; signing takes one click and is recorded against your GitHub account. You only do this once, and it covers everything you've contributed to ProjectSend — past and future.

  • Contributing on your own behalf → Individual CLA
  • Contributing as part of your job, or on behalf of a company → Entity CLA

Why a CLA?

CLAs are contentious in open source and the concern behind that is legitimate, so we'd rather explain this properly than bury it.

You keep the copyright in your contribution. The CLA is a license, not a transfer of ownership. You can continue to use, relicense, or sell your own code however you want. Nothing you grant here takes anything away from you.

It lets the project fund itself. The CLA gives the maintainers the right to also distribute the code under other terms — specifically, a commercial license for organizations whose legal departments can't accept the GPL, and who want to embed ProjectSend in a product of their own. That revenue, along with managed hosting at projectsend.cloud, is what pays for continued development of the free, self-hosted version everyone uses. See LICENSING.md.

It keeps our options open. Without a CLA, changing ProjectSend's license in the future — even to a newer version of the GPL — would require tracking down every contributor from the past fifteen years and getting individual permission. We're not planning a license change. But we'd rather not be permanently unable to make one.

What we commit to in return

  • ProjectSend's core stays under an OSI-approved open source license. Not source-available, not BSL, not SSPL.
  • Nothing that is free today will ever move behind a paid tier.
  • The self-hosted version will never be crippleware. No artificial caps on users, clients, files or storage designed to push you toward the hosted plan.
  • Every release ever published stays available under the license it was published under.

Being straight with you about what we do sell: we operate ProjectSend Cloud, and it has features the self-hosted core doesn't. LICENSING.md sets out exactly where that line is and what stays in the core permanently. If you'd rather your work not go into a project that funds itself this way, that's a fair call, and we'd rather you know before you contribute than after.

If you're not comfortable signing, that's a reasonable position. Open an issue describing the bug or the design instead — that's a real contribution too, and it doesn't require any agreement.

A note on AI-assisted code

If you used an AI coding assistant, that's fine, but you're still making the representations in the CLA: that the contribution is your original work and that you have the right to submit it. Review what you submit and understand it well enough to maintain it.

Third-party code

If your pull request includes code you didn't write, say so explicitly in the description and name the source and its license. Don't quietly paste in a snippet from Stack Overflow or another project — it creates real problems for everyone downstream.

How the code is organised

A modular monolith under app/Modules/ — Identity, Clients, Groups, Files, Sharing, Storage, Notifications, Comments, Audit, Platform and friends. Modules talk to each other through public service classes and events rather than reaching into each other's internals.

Two conventions are worth knowing before your first pull request:

  • File bytes never travel through PHP. Downloads are served by the web server itself (X-Accel-Redirect) or straight from object storage, so a large download does not occupy a PHP worker.
  • Behaviour that differs between installations flows through one registry (app/Modules/Platform/Capabilities/) rather than scattered conditionals. Enforcement derives from it in three places: route middleware, the UI (via the useCapability() hook) and the API, which answers 403 capability_unavailable.

Translations

No user-facing string is hardcoded. English text is the translation key__('Save changes') in PHP, t('Save changes') in React — so English needs no catalogue and a missing translation falls back to English rather than breaking. Every other language is one file: lang/{locale}.json. Framework strings live in lang/{locale}/*.php and come from laravel-lang.

Write your feature in English, and leave the translating to a separate pass. Shipping should never wait on a language nobody in the room speaks, and the cost of a lagging translation is low while the cost of a rushed one is not. Translations are brought up to date on a cadence, and always before a release.

Adding a language is adding a file: a locale is installed exactly when lang/{locale}.json exists. Corrections from native speakers are especially welcome — most of the current catalogues have not yet been read by one.

Questions