Skip to content

FSP as a Multi-Tenancy Concept

FSP stands for Financial Service Provider: a partner bank or microfinance lender — such as Fortune Credit in Kenya or Bancamía in Colombia — that distributes Yaya to its existing customer base, or "Atram" itself acting as a fallback tenant for users who have no partner bank [@business-md]. In this repo, the FSP is not just a label on a user record; it is the unit of multi-tenancy. Every User belongs to exactly one FSP through users.fsp_id, and that single foreign key is what makes it possible for one Laravel application to serve several differently branded, differently configured partner deployments at once [@business-md].

What an FSP owns

The Fsp model is the tenant configuration object. Beyond name, slug, and country, it carries branding fields (logo_path, favicon_url, primary_color, secondary_color, tos_link), loan-product fields (loan_offering_enabled, loan_offering_type, loan_offering_value), and a generic config JSON column [@fsp-model]. It also has four hasOne/hasMany relationships that hang per-tenant configuration off the FSP row: whatsappConfig (a FspWhatsAppConfig), smsConfig (a FspSmsConfig), marketingFlow, and alertConfigs, plus a hasMany to FspBranch for branch-level targeting and a hasMany to users [@fsp-model]. Each of these relationships is a separate tenancy dimension: an FSP can have its own WhatsApp Business account and its own SMS routing, so two FSPs in the same country can send outbound messages through entirely different credentials without any code branching on which FSP it is.

This is why "FSP" is the right word for the concept even though the underlying mechanism is closer to generic multi-tenancy: FSP membership determines which branding a user sees, which WhatsApp/SMS credentials dispatch to them, and which loan product (if any) is offered to them, all read off the single users.fsp_id column [@business-md].

Scoping data by FSP

Because every user belongs to exactly one FSP, most FSP-facing queries need to be scoped so that one FSP's ops team cannot see another FSP's users. Yaya Manager stores the operator's active filter in the session (manager_filter_fsp, manager_filter_country), and two mechanisms apply that filter to queries:

  • User::scopeFilteredByManagerSession() filters the users table directly by country and fsp_id when those session keys are set [@user-model].
  • The FiltersByManagerSession trait provides the same filtering for models that only reach a user indirectly — either through a direct user relation (scopeFilteredByManagerSession) or through a nested relation such as userAction.user (scopeFilteredByManagerSessionVia), by wrapping the filter in a whereHas on the given relation [@filters-by-manager-session].

Both scopes apply the identical rule — filter by country if set, filter by fsp_id if set — so that any model reachable from a User can be FSP-scoped without duplicating that logic per model. User also defaults a user's country from their FSP on creation when it is not explicitly supplied, reasoning that "the FSP is the authoritative source: it operates in exactly one country," which keeps FSP and country scoping consistent even for self-serve signups that never collect a country directly [@user-model].

For the broader vocabulary this concept sits inside — how FSP relates to Atram, Yaya Engine, and Rada — see Atram, Yaya, FSP, and Rada vocabulary.