Skip to main content

Developer Guide

How to build on Flourish Arbor from your own code: find out who is signed in so your pages show the right data, add and remove members from Apex, and work with their roles.

Overview​

The model maps onto a custom portal like this:

Your portalIn Arbor
The portalA network
Each person with accessA member, tied to their Contact (or any record) through SFDC Record ID
Granting accessProvisioning
Showing someone their own dataResolving the signed-in member to that record, on the server

Everything is in the toflourish namespace: toflourish. in Apex, toflourish__ on objects and fields, and toflourish- on component tags.


Who is signed in​

Members are not Salesforce users. To Salesforce every one of them is the site's guest user, so UserInfo tells you nothing. A member is known by their session token, which the browser holds and your Apex checks.

In the browser: the Auth Service​

Flourish Arbor: Auth Service is a component with no screen. Put it in your component's template and call it:

<template>
<toflourish-arbor_-auth-service lwc:ref="arbor"></toflourish-arbor_-auth-service>
<!-- your markup -->
</template>
import { LightningElement } from 'lwc';
import getMyCases from '@salesforce/apex/PortalController.getMyCases';

export default class MyCases extends LightningElement {
cases = [];

async renderedCallback() {
if (this._loaded) return;
this._loaded = true;
const arbor = this.refs.arbor;
if (!arbor.isAuthenticated()) return;
this.cases = await getMyCases({ token: arbor.getToken() });
}
}
MethodReturns
getToken()The session token, or null.
getElement(){ elementId, recordId, elementName, username, email }, or null.
getRecordId()The Id of the record the member is tied to.
getElementId()The member's own Arbor Element Id.
getDisplayName(), getUsername(), getEmail()The member's details.
isAuthenticated()Whether the browser holds a session. It does not ask Salesforce.
validate()Checks the session with Salesforce and renews it. Resolves to the member's details, or null (and clears the session) when it has lapsed.
logout(redirectUrl)Ends the session in Salesforce and the browser, signs this browser out of a persistent sign-in, then goes to redirectUrl or reloads the page.

Get the token from the Auth Service rather than from browser storage. Salesforce can keep each namespace's storage apart, so your component cannot count on seeing Arbor's.

Use what the browser knows for display only. Anything it reports can be edited by the person at the keyboard. Send the token to Apex and decide there.

On the server​

Pass the token to your Apex and resolve it:

public without sharing class PortalController {
@AuraEnabled
public static List<Case> getMyCases(String token) {
Id contactId = toflourish.arbor_AuthUtility.getAuthenticatedRecordId(token);
if (contactId == null) {
throw new AuraHandledException('Please sign in again.');
}
return [SELECT Id, Subject, Status FROM Case WHERE ContactId = :contactId];
}
}
MethodReturns
toflourish.arbor_AuthUtility.getAuthenticatedRecordId(token)The Id of the record the member is tied to, or null.
toflourish.arbor_AuthUtility.getAuthenticatedElement(token)A toflourish.arbor_AuthUtility.AuthenticatedUser with elementId, recordId, displayName, username, email and networkId, or null.

Both return null for a blank, unknown or expired token: treat that as signed out. A token also stops resolving once the member is suspended, the network is set to Inactive, or the sign-in is 24 hours old. Each call renews the session's two hours.

  • Grant your controller to the guest user, on the site's guest user profile, the same way as Arbor's own class.
  • Check the network if your site serves more than one. A token from any network resolves, so compare networkId with the network the page is for.

Events​

When you put Arbor's own components inside your template, they report what happened:

ComponentTagEventdetail
Login Formtoflourish-arbor_-login-formauthenticatedtoken, elementId, recordId, elementName, username, email
Login FormregisterFired instead of navigating, when no Registration URL is set.
Login Formforgotpasswordusername. Fired instead of navigating, when no Password Reset URL is set.
Registration Formtoflourish-arbor_-registration-formregisteredAs authenticated
Auth Guardtoflourish-arbor_-auth-guardauthenticatedelementId, recordId, elementName, username, email
Auth GuardmfarequiredNone. Fired when a magic link needs a second factor, before the redirect to the Login Form.
Auth GuardunauthenticatedNone. Fired before the redirect.

The Login Form navigates away straight after authenticated, so use the event to record something, not to change the page.

Markup you put inside toflourish-arbor_-auth-guard in your own template renders only once the guard has confirmed a session, which saves a component its own signed-out state. That is a convenience, not protection: the server check above is what keeps the data private.


Provisioning from Apex​

The provisioning tool's method is global, so your Apex can call it directly. It takes and returns JSON:

String resultJson = toflourish.arbor_ProvisioningUtility.provisionMember(JSON.serialize(new Map<String, Object>{
'networkId' => networkId,
'method' => 'LINK_ONCE',
'delivery' => 'WELCOME_EMAIL',
'recordId' => contact.Id,
'email' => contact.Email
}));
Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(resultJson);
Key inWhat it takes
networkIdRequired. The network.
methodRequired. LINK, LINK_ONCE, PASSWORD, EMAIL_CODE or USERNAME, allowed on the network.
deliveryNONE (the default) or WELCOME_EMAIL.
recordIdThe record to tie the member to.
firstName, lastNameCreate a Contact when there is no recordId.
email, username, displayNameThe member's details. username defaults to email.
linkTtlSecondsHow long a LINK_ONCE link lasts. Defaults to seven days.
rolesA list of role names, for example new List<String>{ 'Staff' }. Each must be one of the network's Roles, in any case. An unknown role refuses the call before anything is created.
Key outWhat it holds
successWhether the member was created.
elementIdThe new member.
recordIdThe record they are tied to.
authStatusProvisioned or Invited.
loginUrlThe link, when delivery is NONE and the method is a link method.
errorMessageWhy it failed.
existingElementIdWhen it failed because the username or email already belongs to a member of the network, that member.

The rules are the ones in Members. A Lightning web component can call the same method, as @salesforce/apex/toflourish.arbor_ProvisioningUtility.provisionMember with a jsonInput argument, for anyone holding Flourish Arbor: Super Admin.

Someone who is already a member is refused, not duplicated, so calling it again for the same person is safe. Branch on existingElementId: skip them, or reactivate them with setMemberStatus if they were suspended.


Suspending and reactivating​

The other half of provisioning, also global:

String resultJson = toflourish.arbor_ProvisioningUtility.setMemberStatus(JSON.serialize(new Map<String, Object>{
'elementId' => elementId,
'status' => 'Suspended' // or 'Active' to reactivate
}));
// { "success": true, "elementId": "...", "authStatus": "Suspended", "errorMessage": null }

Any other status is refused with status must be "Suspended" or "Active". What suspending does is described in Members; admins can also do it by hand in Member Access. There is no Flow action for it, so call it from a trigger, or from an invocable method of your own when a Flow needs it.


Roles​

A member's roles are in the toflourish__Roles__c field on their Arbor Element, separated by semicolons. There is no special method for changing them: update the field from Apex, or from a Flow's Update Records.

update new toflourish__Arbor_Element__c(Id = elementId, toflourish__Roles__c = 'Staff;Volunteer');

Every write is checked against the network's Roles, however it is made. A role the network doesn't list fails the save with a message naming the network's roles, and the roles that pass are saved in the network's spelling.

When you read the field, keep only the names the network still lists. A role removed from the network stays on its members' records until their roles are next saved, and it grants nothing. Read both fields and intersect them, ignoring case. Treat a suspended member, or one still to set up a password, as holding none.

A Flourish Core portal does all of that for you, and reads members' roles with a single rule in its configuration.


Recipe: a portal for Contacts with access​

  1. Create the network. Allow One-Time Magic Link, set Auth Site URL to a page carrying the Auth Guard, and set Login Redirect URL.
  2. Grant access automatically. A record-triggered Flow on Contact, set to run only when a record is updated to meet your access rule, calls Provision Arbor Member with Delivery WELCOME_EMAIL, Record ID {!$Record.Id} and Email {!$Record.Email}. To bring existing Contacts in, run a scheduled Flow once.
  3. Grant access by hand, if staff need to. A small component on the Contact page calls provisionMember with the page's record Id. For someone already a member it gets existingElementId back, and can send staff to that record's Member Access panel.
  4. Build the pages. The Login Form on a sign-in page, the Auth Guard on every portal page, and the Request Link page for members whose one-time link is spent.
  5. Scope the data. Every controller resolves the token with getAuthenticatedRecordId and queries against that Contact.
  6. Take access away. A Contact trigger calls setMemberStatus with Suspended when the access rule stops holding.

What to leave alone​

  • The credential fields on Arbor Element (password hash and salt, authenticator secret, link token and its expiry, and the cache field) belong to Arbor. Keep them off layouts and components, and never write to them.
  • The Product Server permission set is for one connected app's run-as user, not for your controllers or the guest user.