Membership plans

A Xuda plan is one record with fifteen fields, and the backend reads four of them.

The seven membership rungs are records in PLAN_OBJ: config.json:1820 free, :1850 pro, :1880 team, :1910 agency, :1940 enterprise_t1, :1969 enterprise_t2, :1998 enterprise_t3. Each one carries an identical features map of fifteen keys, five numeric and ten strings, and the key set is the same fifteen on all seven, so a rung without a capability spells it N/A rather than dropping the key. Four of those fifteen are read by running code: projects and team_members by validate_user_plan (account_module:3932 and :3947), drive and ai_credits by get_effective_entitlements (:666-701). The other eleven, including all eight Available and N/A capability strings, are read by nothing. What decides whether a method runs is a separate registry: 30 of the 1010 entries in config.json cpi_methods carry a min_plan, and http_module compares plan ranks against it on every request (:542-547, enforced at :868). Membership is also only one of sixteen plan categories in the same object, 7 records of 72, so the rest of the price is a set of ladders that stack beside it rather than inside it.

Seven rungs, fifteen keys, four enforced
Thirty gated methods out of 1010
Sixteen plan categories, membership is one
A Xuda plan is one record with fifteen fields, and the backend reads four of them.
15

Feature keys on every membership record, the identical set on all seven

4

Of those fifteen that any running code reads

30

Of 1010 cpi_methods carrying a min_plan, the gate that actually refuses

16

Plan categories in PLAN_OBJ, and membership is one of them

The seven rungs

The seven rungs and the five numbers on each, exactly as stored.

These are the five numeric keys off the features map of each membership record, copied across without reformatting. A field called plan_category splits the seven 4 standard and 3 enterprise, and each record also holds a one word title: Try, Deploy, Colab, Sale, Sync, Host, Own. A note field is set on two records only, pro reading Best Value and team reading Most Popular, while agency carries an empty string. Instances is printed as 0 on the bottom three rungs rather than left blank, because agency at 500 being the first non-zero is the point of that row. Only enterprise_t3 uses the string unlimited, and it uses it in all five cells; planFeatures.js:5-15 states the reading rule the platform holds itself to, that a number is always a real cap and only that literal string means unlimited, which is why no large number on this table is redrawn as the word. The honest limit belongs in the same paragraph: PLAN_OBJ carries no billing period field on any of the seven records, so the per month reading is the renderer, pricing.vue:129 and :359, and not the record.

Projects
Team members
Instances
Drive GB
AI credits
Free, $05101530
Pro, $49502050600
Team, $14925010002501600
Agency, $3991000100050010003500
Enterprise T1, $1999500050002000500010000
Enterprise T2, $4999100001000050001000050000
Enterprise T3, callunlimitedunlimitedunlimitedunlimitedunlimited
What reads the record

Fifteen keys go in. Four come out.

The features map is not a settings object the platform consults. It is fifteen keys of which four have a consumer, and the four do not behave alike: two of them refuse a call with a code, and two of them only raise a number that something else spends or bills. The remaining eleven have no reader anywhere in cpi, dashboard/src or shared.

The honest limit on the three rows that end in nothing. Absence of a grep hit across cpi, dashboard/src and shared is strong evidence but it is not proof against a key composed at runtime. No dynamic features-key access was found in any of the three trees, which is the most this method can establish.
The two that bite

Two of the fifteen can stop you, and both are a plain greater-than-or-equal.

validate_user_plan (account_module:3891) does not call get_effective_entitlements and shares no code with it. It counts the account master apps from a CouchDB view, reads PLAN_OBJ[membership_plan].features straight off the record and compares. Both branches fire only when app_obj.app_type is master, and the account own system project is exempted by !app_obj.is_account_project, so an account sitting at its cap can still log in rather than being locked out of the thing that would let it upgrade.

  • Projects: the comparison is ret_obj.master >= plan.features.projects at :3932, and it is called from app_module:1282 and :1301, which is the moment a project is created.
  • Team members: the view count >= plan.features.team_members at :3947, called from team_module:265, :675 and :1280, so every share path runs through the same line.
  • Advertising campaigns are exempted outright at :3894, and any app_type other than master bypasses both caps entirely. Which app types reach this path in practice was not traced, and that is UNVERIFIED.
  • Two of the team-share paths never reach a clean refusal at all: team_module:271 and :1286 each execute return ret; with a comment already sitting in the source reading ret not defined, so the seat cap throws a ReferenceError instead of returning the -9 object. What a user finally sees on those two paths is UNVERIFIED.
What a create call walks through
  1. Count the master apps
    From a CouchDB view, with the account own system project excluded
  2. Read the plan record
    PLAN_OBJ[membership_plan].features, straight off config, no entitlement helper
  3. Compare, greater or equal
    ret_obj.master >= plan.features.projects, and the create call stops there
  4. Count the active team shares
    The second view, the second comparison, the second refusal code
What the refusal says

The two quota refusals are not the polished sentence the capability gate writes.

The capability gate composes its message from the method display name and the PLAN_OBJ display names (http_module:555-562), so it names the feature, names the plan and names the upgrade. The two quota refusals do none of that. They are template literals that interpolate the internal plan key rather than resolving it, they are ungrammatical, and they point at no upgrade path. app_module:1287 and :1306 return the object to the caller unchanged, so nothing between the check and the client rewrites them. The -8 line below is printed with one concrete set of values, an agency account at its 1000 project cap; everywhere else the angle brackets stand for a field filled in at call time, which is the only honest way to print a string whose values do not exist until the call.

account_module:3933 . the project cap
{ code: -8, data: "Number of projects (agency) exceeds to the 1000 plan limits" }
account_module:3948 . the seat cap
{ code: -9, data: "Number of team shares (<count>) exceeds to the <cap> plan limits" }
http_module:562 . same product, different voice
{ code: -1, data:
"<Feature> isn't available on the <Plan> plan. Upgrade to <Plan> or higher to use it." }
the quota strings interpolate the plan key, the gate string resolves the display name
Whether the dashboard rewrites the -8 and -9 strings before display is UNVERIFIED. What is certain is that the backend hands out three different registers for the same event, a customer being told to buy something.
The word unlimited

The word unlimited is handled twice, and only one of them is on purpose.

Enterprise T3 is the only rung whose five numeric quotas are the string unlimited rather than a number, and two different functions meet that string on two different code paths. dashboard/src/lib/planFeatures.js:5-15 states the contract the platform means to keep: a number is always a real cap, and only the literal word unlimited means unlimited. get_effective_entitlements keeps that contract deliberately. validate_user_plan keeps it by accident. The two lanes below answer the same four questions in the same order and arrive at the same place, which is exactly why the difference is worth drawing rather than describing.

Running the comparison directly prints false. Both lanes reach the right answer and only the left one is written deliberately, which means a mistyped quota value on any rung would read as unlimited rather than as an error.
Honest limits

What this page cannot tell you about the numbers above.

  • Everything above was read from repository source on branch main with uncommitted work in the tree, and nothing here was executed against a live account. Where the repo and a running box disagree, this page is wrong and the box is right.
  • PLAN_OBJ carries no billing period field on any of the seven records. The monthly reading comes from the renderer, website-nuxt/pages/pricing.vue:129 and :359, rather than from the record, so every price above is monthly only because the page that renders it says so.
  • No annual rate exists anywhere in PLAN_OBJ. pricing.vue:476 says annual is discounted and to ask sales, so no annual figure and no annual percentage appears anywhere on this page. A discount we cannot read from a record is a discount we will not print on a card.
  • Whether the seven price_id strings resolve to live Stripe prices at these amounts was not checked. Only the strings in config were read. The price_id also differs per environment, which is why no page should ever surface one.
  • Memory records config.json as diverging from the live master config, which was not read here. Every figure above is repo-authoritative only, and the repo and dev copies agree on all seven records except the Stripe ids.
  • Enforcement below this line was traced only as far as the HTTP boundary. Whether deploy_module re-checks datacenter or load-balancer eligibility further down for a Pro account is UNVERIFIED, so read the next block as what the front door decides.
The gate

The gate that actually refuses a feature never opens the plan card.

The features map on a membership record is a description of a plan, not a mechanism. The mechanism is a second registry entirely, cpi_methods in config.json, read on every inbound call by http_module. It never looks at the features map, it looks at one optional field on the method being called, and it decides with a single rank comparison.

The same rank comparison is mirrored client-side at shared/utils.js:1020-1030 so the dashboard can grey a control before the request goes out, and the server one is the one that decides.
Claims against gates

Eight capability strings, five enforced somewhere else, three enforced by nothing.

Ten of the fifteen feature keys are strings, and eight of those ten are binary: N/A until a rung turns them on, then on for every rung above, with the ladder strictly monotone so nothing on at rank N is off at rank N+1. Because agency turns on two of them and enterprise_t1 turns on two, the eight gates land on six distinct rungs rather than eight. None of the eight strings is read by any code. What follows is what each one is actually worth.

What the plan card claims
What actually enforces it
Verdict
deployN/A on Free, Available from Promin_plan pro on deploy_spa, deploy_mpa, deploy_pwa, deploy_embed, deploy_api, deploy_cron, deploy_emitter, deploy_preview, deploy_user_group and deploy_anotherMatches, except deploy_mini_app carries min_plan free
marketplaceN/A until Agencymin_plan agency on deploy_marketplace, deploy_instance, create_marketplace_listing, update_marketplace_listing and update_marketplace_listing_statMatches
upload pluginN/A until Agencymin_plan agency on upload_pluginMatches
datacenter syncN/A until Enterprise T1, $1,999min_plan pro on deploy_datacenter, which is $49The card is stricter than the code, and the code is what runs
load balancerN/A until Enterprise T1, $1,999min_plan pro on deploy_load_balancer, which is $49The card is stricter than the code, and the code is what runs
outsourced serverN/A until Enterprise T2Nothing. Zero hits across cpi, dashboard/src and shared under any spellingA claim, not a gate
xuda sdkN/A until Enterprise T3Nothing, by the same exhaustive searchA claim, not a gate
ai priorityN/A until TeamNothing, by the same exhaustive searchA claim, not a gate
The registry

Thirty methods out of 1010 carry a plan floor, and none of them is above Agency.

The gate registry is cpi_methods in config.json: 1010 entries, 30 carrying a min_plan and one carrying a min_support_plan. The floors are pro on 19, agency on 6, team on 4 and free on 1. No method anywhere carries a min_plan above agency, so the three enterprise rungs unlock nothing at this gate and what they change is the five numbers. One floor in config is simply wrong: set_credit_rules reads min_plan pro while account_module:9339 refuses anything below Team with the words Credit management is available on the Team plan. The code is the stricter gate.

  • Pro, 19 methods. Ten are the deploy family: deploy_spa, deploy_mpa, deploy_pwa, deploy_embed, deploy_api, deploy_cron, deploy_emitter, deploy_preview, deploy_user_group and deploy_another. The other nine are deploy_datacenter, deploy_load_balancer, app_visibility_off, set_login_screen, generate_login_screen, change_under_attack_mode, enable_app_branding, create_stripe_account_link and set_credit_rules.
  • Team, 4 methods: add_developer_member, add_admin_member, add_login_sponsor and list_sponsored_users. So whether you may invite a developer is the rank, and how many you may invite is the team_members number. Two mechanisms on one screen.
  • Agency, 6 methods: upload_plugin, deploy_marketplace, deploy_instance, create_marketplace_listing, update_marketplace_listing and update_marketplace_listing_stat.
  • Free, 1 method: deploy_mini_app. A floor that admits everybody, and the reason the features map calling deploy N/A on Free is not the whole story.
Four floors
  1. free
    1 method, deploy_mini_app, gated at a floor everyone already meets
  2. pro, rank 1
    19 methods, the deploy family plus nine app and account controls
  3. team, rank 2
    4 methods, all of them about adding a person to the account
  4. agency, rank 3
    6 methods, marketplace and plugin publishing, and the highest floor in the file
Stacking

Sixteen ladders share one object, and exactly two of them change your allowance.

PLAN_OBJ holds 72 plan records across 16 categories, and membership is 7 of them. One function answers what an account is actually entitled to, and it reads three plan fields out of the sixteen: membership_plan, ai_workspace_plan and drive_plan. Everything else in that object gates its own module on its own axis and never touches your credits or your storage.

The function is account_module:666-701, 36 lines. The asymmetry is deliberate and the comment at :660-664 says why: withholding space which has its own line on the invoice would be selling nothing. Whether checkout stops a free-membership account from buying a paid workspace tier was not traced. UNVERIFIED.
Sixteen ladders

The sixteen ladders, their rungs and their ceilings.

PLAN_OBJ holds 72 plan records across these sixteen categories, and membership is only 7 of them. The other fifteen hold 65 rungs between them, and every single one of the fifteen has a $0 rung, so each ladder is a choice rather than a wall you hit. Prices below are the price field on the records, monthly USD list prices in the repo config. The third column is where the argument is: exactly two of the fifteen change what your membership allowance is worth, because get_effective_entitlements reads only membership_plan, ai_workspace_plan and drive_plan and returns only ai_credits and drive_gb. The other thirteen gate their own module, their own products, mailboxes, tickets and commission, and never touch your credits or your storage. Xucode is the one product gated by membership rank rather than by its own ladder: xucode_entitlement (deploy_module/xucode.mjs:32-66) compares the account rank against config.json:414 min_membership pro, and an unrecognised membership id fails closed, meaning no machine rather than a free machine.

Rungs
Top monthly price
Changes your membership allowance
membership7$4999, and the top rung is the word callIt is the allowance
ai_workspace8$230Yes, credits and storage, on a paid membership only
drive4$39Yes, storage, on every tier including free
profile_phone6$39No
support4$4,500No
trust_center4$299No
commerce4$149No
shipping4$149No
tickets4$149No
marketplace4$149No
finance4$79No
bot_protection4$59No
xucode3$59No, and it is the only ladder with a membership floor
email4$49No
auto_response4$49No
static_website4$39No
Honest limits, second axis

Five things about the other fifteen ladders this page will not print.

  • Two ai_workspace rungs disagree between config.json and config_dev.json by two orders of magnitude, and they are the only two price differences in all 72 records, so no figure for either appears anywhere on this page.
  • Every paid rung of email, tickets, trust_center and auto_response carries no price_id at all, and three of the five paid phone rungs do not either. stripe_module:251-258 treats a missing price_id as money that is not a subscription line, and it names phone as billed per number and email per mailbox. No such per-resource charge is named for tickets, trust_center or auto_response, so whether those three are chargeable today is UNVERIFIED.
  • config.json:414 sets xucode.enabled false alongside min_membership pro, so Xucode has three priced rungs, a Pro floor and may not be purchasable at all right now.
  • The member perk is one price swap on one category. Every paid ai_workspace record carries a member_price_id, and no other category and no membership rung has one. The source comment at stripe_module:211-223 describes it as 10 percent off, carried as a 90 percent price on the same Stripe product. The amounts live in Stripe rather than in the repo, so no member dollar figure appears here.
  • Many add-on rungs carry a placeholder price_id, and stripe_module:226-300 now mints a real Stripe price from the catalog record own price field on first sale, with a deterministic lookup key so a restart or another region finds the same price rather than minting a second. That is why the config number is the charged number for those ladders. Read from source, never exercised against live Stripe.
Upgrading

An upgrade collects the money before it grants the plan.

Every account starts on the free membership item, so buying a tier is a reprice of that one subscription item rather than a new subscription, and stripe_update_plan decides direction by comparing rank inside the category (:1770-1787). What follows is not a plan change that then bills you. It is a charge that, only once it clears, becomes a plan change.

A promo code, when one is passed, is applied to the subscription after the change, so it discounts renewals and never the immediate proration charge (:725-728). Tax is not part of the preview envelope, so the displayed figure is the proration lines only.
The checkout summary

The number on the button is the number that is charged.

stripe_get_proration returns a normalized envelope rather than a raw Stripe invoice, carrying direction, plan_name, currency, renewal_amount, renewal_date, amount_due_now and itemized proration_lines (:446-500), and the checkout summary renders exactly that and nothing else. The code comment records the bug this fixed: the old version showed the whole next invoice while it only charged the delta.

checkout summary . direction: upgrade
<Plan> prorated for the rest of this cycle
Amount due today $<amount_due_now>
Then $<renewal_amount>/month from <renewal_date>
[ Pay $<amount_due_now> ]
the preview is computed without the promo code on purpose, so this figure equals what is charged
checkout summary . direction: downgrade
Due today $0.00
[ Schedule downgrade ]
and afterwards, on the billing screen:
Scheduled . Downgrades to <Plan> on <date> [ Cancel ]
a non numeric price renders Contact us for <plan> pricing instead of a number
A downgrade writes a two-phase Stripe schedule: phase 0 is the exact item set you hold today running to the natural period end, phase 1 the same set with only that one item swapped, for one iteration, then end_behavior release hands the subscription back to normal renewal (:588-596). Nothing is charged now, the paid plan runs to period end, and stripe_cancel_scheduled_change releases the schedule at any point before it lands (:775-802). No method anywhere in stripe_module cancels a membership subscription: leaving a paid tier is a downgrade to free, because free membership, starter support and free workspace are real $0 lines on the same subscription (:2239-2243). The Cancel above is the other kind, releasing a scheduled change rather than ending the plan.
When a card fails

A failed card holds the account before it suspends anything.

Two flags on the account document carry the whole of it, account_billing_hold_status and account_suspension_status. Both are set by the invoice.payment_failed webhook and both are cleared by a different webhook rather than by anything a support agent does by hand. The distance between the first and the second is three more attempts on the same invoice, and only the second one stops serving your apps.

  1. 01

    First failure: a billing hold

    account_billing_hold_status is set to 1 with a timestamp and an account log entry (stripe_module:1945-1949), and the same handler emails the customer. The dashboard shows a banner listing the unpaid invoices and the total due with a Pay all button, and says some account actions are blocked, which is the honest phrasing because the full list is not enumerated in one place. Marketplace refuses a purchase outright while the hold is on (marketplace_module:1252-1254).

  2. 02

    Fourth attempt: suspension

    When webhook_data.attempt_count reaches 4 the same handler also sets account_suspension_status (:1953-1958). That one is not cosmetic: router_module:5697 throws Account suspended and stops serving the account apps. The retry schedule behind attempt_count is Stripe dashboard configuration and is not in the repo, so calendar time to suspension cannot be pinned from source. UNVERIFIED.

  3. 03

    Payment succeeds: both released

    customer.subscription.updated clears both flags once no subscription on that customer is in a non-active state (:1806-1822). Two exemptions never reach any of this: the one-off proration upgrade invoice is tagged so a declined upgrade leaves the current plan healthy, and an owner and superuser allowlist is never auto held or auto suspended.

Entitlement

Why is this feature not in my plan?

Six places where the plan card and the code disagree, answered from the code, with the unsettled ones marked.

Where to go next

This page explains the shape. These three answer the price.

Everything above this line is mechanism: which comparison can refuse you, which registry decides a capability, which two of the fifteen add-on ladders touch your allowance and which thirteen do not. None of it tells you what to press. Three links do. The first is the tier picker with the checkout flow behind it, which is the one question this page deliberately does not answer: a page arguing that the plan card is not the enforcement layer should not also be the page selling the plan card. The second is the sibling read on the single add-on that moves your credit number. The third is your own account, where the strings quoted above are rendered against your data rather than against an example. That entry is the @dashboard sentinel rather than a URL, and it goes through the same resolver as every other link here, so it lands wherever your dashboard actually lives.

Walkthrough

The billing screens, in motion.

This is the recording the previous membership-plans document already carried on its youtube field, kept here rather than dropped. A page assembled out of config keys, rank comparisons and refusal strings is a poor way to watch a billing screen move, and the walkthrough is the part of the old document that nothing in this rewrite replaces. Two rules for watching it. Where the walkthrough and this page disagree about a figure, the page is the one built from source: the seven membership records in PLAN_OBJ, the thirty method floors in cpi_methods, and the enforcement paths in account_module, http_module and stripe_module. And where the walkthrough shows a plan card listing a capability, hold on to what the whole page above has been about. The card is a set of config strings that no code reads, the gate that actually runs is a rank comparison against a method floor, two of the eight capability strings are gated lower than the card claims and three are gated by nothing found anywhere in cpi, dashboard or shared. A tour of the screens cannot show you any of that, because a screen recording can only ever show you the card. It sits after the mechanism rather than in place of it for that reason, and it is worth watching for the shape of the flow, where the plan rows sit, what a scheduled change looks like when it is pending, and what the checkout preview puts in front of you before it charges anything.

Recorded before this page was written, so treat any figure on screen as accurate to its recording date rather than to the config read above. Every number on this page came out of config.json and the cpi source beside it rather than out of this video. The repo copies of config.json and config_dev.json are also known to diverge from the live master config, so a price spoken aloud in the walkthrough, a number in the tables above, and the amount an account is actually charged are three separate claims. Only the middle one was verified here.

Open the billing screen and read your own record.

Your membership is one string on your account document, membership_plan, holding the key of one of seven records, and everything above is what that string gets compared against. Two comparisons can refuse you outright, and both are plain numeric tests on the features map: your project count against features.projects and your active team shares against features.team_members, at account_module:3932 and :3947. Both print a sentence naming the cap, returned as code -8 and code -9, and both name the internal plan key rather than the display name. Every capability is decided somewhere else entirely, by a registry of thirty method floors in cpi_methods compared by rank at http_module:868, and that comparison never reads the plan card at all. Two of the other fifteen ladders change your allowance, AI Workspace and Drive, and thirteen do not, because get_effective_entitlements reads exactly three plan fields and returns exactly two numbers. Four things are now checkable on your own screen rather than on a promise. The plan row shows the tier your account actually holds. A scheduled change shows its effective date and the Cancel button that releases it, right up until it lands. The consolidated subscription shows membership, support and AI Workspace as lines on one invoice rather than as three bills. And the credit balance printed beside the tier is a ledger figure, which is exactly why it is not the number on the plan card. Nothing is charged by looking. If your membership is free, note the one stacking rule while you are in there: the AI Workspace add-on is counted only when the membership is set and is not free, so a paid workspace tier on a free membership adds neither its credits nor its storage. Whether checkout stops that purchase from happening in the first place was not traced, and this page says so rather than assuming it does.