Organizations
Per-org packages, tokens, members, and isolation. Every install ships with a `default` org; add more for per-team or per-customer isolation.
Updated April 27, 2026
Every Packyard install runs the same shape: a super-admin user plus at
least one organization. The first boot seeds an org with slug=default
and the admin as its owner; from there you can rename it or provision
more orgs for per-team or per-customer isolation. There's no flag to
toggle — orgs are first-class on day one.
Composer URLs are always slug-prefixed:
/{slug}/packages.json/{slug}/p2/<vendor>/<package>.json/{slug}/dist/<vendor>/<package>/<version>/<sha>.zip
For a solo / single-team install, that's just /default/... everywhere
and you can stop reading. The rest of this page is for installs that
need more than one org.
Organization fields
Each organization has:
- Slug — the URL-safe identifier (e.g.
acme,globex-staging). Cannot be changed once created. - Display name — human-readable label shown in the dashboard.
- Status —
active,suspended, orarchived. Suspended orgs return 402 on every Composer request; archived orgs return 410. - Members with roles:
owner— full control (manage packages, tokens, members, settings).member— manage packages and tokens within the org.
- Tokens — scoped to this org only. A token from
acmecannot accessglobexpackages, full stop.
Super-admins (set via packyard admin user create --super-admin) can manage every org from the /admin dashboard section or the /api/admin/* API.
Creating an organization
From the dashboard:
- Sign in as a super-admin.
- Open Super-admin → Organizations → New.
- Enter slug + display name. Submit.
- Add an owner via Members → Add member with role
owner.
From the API (an admin token is required — mint one in the dashboard):
curl -X POST https://repo.example.com/api/admin/orgs \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug":"acme","name":"Acme Inc."}'
curl -X POST https://repo.example.com/api/admin/orgs/acme/members \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","role":"owner"}'The full admin API surface is documented in AGENTS.md in the product repo.
Status transitions
active ↔ suspended ↔ archived are the three states. Use them to gate access to an org without deleting any data:
curl -X PUT https://repo.example.com/api/admin/orgs/acme/status \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"suspended"}'Suspended Composer requests return 402 Payment Required (semantically appropriate for billing-driven suspensions). Archived orgs return 410 Gone. Both states preserve all packages, tokens, and members — flipping back to active restores access without re-provisioning.
Per-org isolation
Token scope is enforced at request time:
- A request to
/acme/packages.jsonwith a token belonging toglobexreturns 403. - Cross-org URL guesses do not leak metadata — unknown slugs return 404 even for valid tokens.
Storage is also scoped: package zips live under <storage-root>/<org-slug>/<vendor>/<package>/<version>.zip (or the equivalent S3 key prefix). Migrating an org between Packyard instances is a flat copy of that subtree plus a DB export of its rows.
When to add more orgs
Add an org when any of these is true:
- You host registries on behalf of clients with separate billing or compliance boundaries.
- You run a single Packyard instance that needs to serve multiple teams that shouldn't see each other's packages.
- You want one DNS name (
repo.example.com) to serve isolated Composer surfaces — no per-team subdomains, no extra processes.
If a single team is the whole audience, the seeded default org is all you need. You can rename its slug to something more descriptive (e.g. your team or company name) and never think about orgs again.