Packages
Audience: Agency-success / Sales / Management · Where in app: /agency/packages (list, create, edit) · Plan availability: Agency tier; route gated by agency.manage_packages
Packages here are the client-facing plans an agency defines and assigns to its sub-accounts — "Basic", "Pro", etc. The agency sets the user count, AI-credit allotment, and renewal window; it does not set a Stripe price here (the agency bills its clients off-platform, or via whatever the agency arranges). This is distinct from agency packages (AgencyPackageModel), which are the SKUs the agency itself subscribes to and which grant sub-account slots — those live in Subscriptions.
- Client packages (this page) —
PackageModelrows scoped byagency_id. Created atPOST /agencies/packages. Fields:title,description,default_users,ai_credits,expiry_days,user_emails. No Stripe price;visibility: private,base_price: 0. - Agency packages —
AgencyPackageModelrows withamount,currency,sub_accounts,stripe_price_id. These are the platform's plans the agency buys to get sub-account capacity. See Subscriptions. Don't confuse the two — the code paths and models are different.
What it does
- Lets an agency create / edit the packages it offers clients.
- Each package defines default user seats and a monthly AI-credit allotment granted to any client placed on it.
- Optionally restricts a package to specific client emails (
user_emails). - Packages appear in the company-create and package-assign dropdowns (see Managing Companies).
How it works
The package list/create/edit screens are gated by the route meta agencyPermission: 'agency.manage_packages'. The Edit screen reuses the platform Billing repository (getPackage, createPackage, updatePackage) but the create/update endpoints are the agency ones, which stamp agency_id and force visibility: 'private', base_price: 0, currency: 'USD', status: 'active'. So a client package is a private, agency-scoped PackageModel row with no purchasable Stripe price — it's an allocation template, not a checkout SKU.
When the package is assigned to a client (during company create or via assign-package), the client's SubscriptionModel is created/updated with package_id and remaining_ai_credits = pkg.ai_credits. AI-credit and seat limits then flow from the package onto the sub-account.
Source: frontend/src/views/agency/packages/Edit.vue and AgencyController (createCompanyPackage, updateCompanyPackage).
Configuration & options
Package fields (packages/Edit.vue)
| Field | Default | Required | Notes |
|---|---|---|---|
Title (title) | '' | yes | e.g. "Basic Plan" |
| Description | '' | yes | Shown to client |
Expiry Days (expiry_days) | 30 | no (defaults to 30) | Days until the client subscription needs renewal |
Default Users (default_users) | 5 | yes | Seats included; min 1 |
AI Credits (ai_credits) | 1000 | yes | Monthly AI credits granted to the client; min 0 |
User Restrictions (user_emails[]) | [] | no | If set, package is only available to these emails; empty = available to anyone. Empty strings are filtered on submit |
Validation is vee-validate + zod (title/description non-empty, default_users ≥ 1, ai_credits ≥ 0, expiry_days ≥ 1, valid emails).
Backend create/update
| Action | Endpoint | Controller | Fields persisted |
|---|---|---|---|
| Create | POST /agencies/packages | createCompanyPackage | agency_id, title, description, default_users, ai_credits, user_emails, expiry_days (default 30), base_price: 0, currency: 'USD', status: 'active', visibility: 'private' |
| Update | PUT /agencies/packages/:id | updateCompanyPackage | title, description, default_users, ai_credits, user_emails, expiry_days |
| List | GET /agency-packages (getPackages) | — | Packages available to the agency |
Note: the read repository in
frontend/src/repositories/agency/index.js(getPackages) hits/agency-packages, while the Edit screen reads individual packages through the platform Billing repo (getPackage). Both surfacePackageModeldata; the agency-scoped writes are what make a package "a client package".
Duplicate mode
Edit supports ?duplicate=<id> — pre-fills the form from an existing package, titles it "
Behaviors & edge cases
- No price field. Client packages carry
base_price: 0andcurrency: 'USD'; the agency handles client billing separately. Don't expect a Stripe checkout for these. - Private + agency-scoped. Created packages are
visibility: 'private'and taggedagency_id, so they never appear in the public platform pricing and aren't visible to other agencies. expiry_daysdefaults to 30 server-side even though the model default is 7 — the agency controller overrides it when omitted.- Email restriction is a filter, not an invite.
user_emailslimits which client emails can be placed on the package; it does not send anything. - AI credits are per-package. Changing a package's
ai_creditsaffects newly-assigned/renewed clients; existing clients refresh on their renewal cycle (see Managing Companies). - Permission gate. Staff without
agency.manage_packagescan't reach these routes (route meta), and the backend write checksisAgencyUser.
Plan & limits
- The agency tier governs whether packages can be created at all (verify).
- Seat (
default_users) and credit (ai_credits) numbers are agency-chosen, not platform-fixed — there is no documented ceiling in this code (verify any platform cap).
Technical implementation
- Views:
frontend/src/views/agency/packages/{List,Edit}.vue - Routes:
frontend/src/router/agency.js(agency-packages,-create,-edit; metaagencyPermission: 'agency.manage_packages') - Repositories:
frontend/src/repositories/agency/index.js(getPackages,createPackage,updatePackage),frontend/src/repositories/Billing(getPackage) - Controller:
backend/app/controllers/AgencyController.js(createCompanyPackage,updateCompanyPackage) — see backend service - Models:
PackageModel(client packages,agency_id-scoped), contrastAgencyPackageModel(agency's own plans)
Related
- Agency Portal — Overview
- Subscriptions — the agency's own plans (
AgencyPackageModel) - Managing Companies — assigning packages to clients