Skip to main content

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.

Two different "package" concepts
  • Client packages (this page)PackageModel rows scoped by agency_id. Created at POST /agencies/packages. Fields: title, description, default_users, ai_credits, expiry_days, user_emails. No Stripe price; visibility: private, base_price: 0.
  • Agency packagesAgencyPackageModel rows with amount, 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)

FieldDefaultRequiredNotes
Title (title)''yese.g. "Basic Plan"
Description''yesShown to client
Expiry Days (expiry_days)30no (defaults to 30)Days until the client subscription needs renewal
Default Users (default_users)5yesSeats included; min 1
AI Credits (ai_credits)1000yesMonthly AI credits granted to the client; min 0
User Restrictions (user_emails[])[]noIf 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

ActionEndpointControllerFields persisted
CreatePOST /agencies/packagescreateCompanyPackageagency_id, title, description, default_users, ai_credits, user_emails, expiry_days (default 30), base_price: 0, currency: 'USD', status: 'active', visibility: 'private'
UpdatePUT /agencies/packages/:idupdateCompanyPackagetitle, description, default_users, ai_credits, user_emails, expiry_days
ListGET /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 surface PackageModel data; 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 " (Copy)", and creates a new one on submit.

Behaviors & edge cases

  • No price field. Client packages carry base_price: 0 and currency: 'USD'; the agency handles client billing separately. Don't expect a Stripe checkout for these.
  • Private + agency-scoped. Created packages are visibility: 'private' and tagged agency_id, so they never appear in the public platform pricing and aren't visible to other agencies.
  • expiry_days defaults 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_emails limits which client emails can be placed on the package; it does not send anything.
  • AI credits are per-package. Changing a package's ai_credits affects newly-assigned/renewed clients; existing clients refresh on their renewal cycle (see Managing Companies).
  • Permission gate. Staff without agency.manage_packages can't reach these routes (route meta), and the backend write checks isAgencyUser.

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; meta agencyPermission: '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), contrast AgencyPackageModel (agency's own plans)