Sharing

Share vault records with other accounts through invites, and manage who has access.

Preview

This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.

The owner of a record shares it by creating an invite. The recipient previews the invite, accepts it with the invite's secret and, when the invite asks for one, a passcode. From then on the record appears in the recipient's vault with permission View. The owner can list and revoke invites and take access away again.

Sharing runs through the same store as the rest of the vault: dispatch VaultAction.Share with a SharingRequest on VaultStore and read the result from the sharing field of its state. Sharing needs a signed-in session: in a transient session every sharing request fails with TransientNotSupported.

How an invite travels

  1. The owner dispatches CreateInvite for one of their records. The result, sharing.created, holds the invite and its secret.
  2. Your app delivers the invite's linkId and the secret to the recipient, and the passcode, if the invite has one, through a separate channel. The SDK does not build or send the message.
  3. The recipient dispatches PreviewInvite with the linkId. The result, sharing.preview, shows the invite, including its id, status and whether it asks for a passcode.
  4. The recipient dispatches AcceptInvite with the invite id, the secret and the passcode. The SDK unwraps the record key with the secret, stores the record in the recipient's vault and sets sharing.acceptedRecordId.

Only the holder of the secret can open the record: the invite carries the record key sealed under a key derived from the secret, and the secret itself never reaches Folio in the clear.

Requests

RequestFieldsResult in sharing
LoadInvitesnoneowned and incoming are replaced.
LoadAccessrecordId: Stringaccess is replaced with the grants of that record.
CreateInviterecordId: String, invite: InviteRequestcreated is set to the new invite and its secret.
PreviewInvitelinkId: Stringpreview is set to the invite.
AcceptInviteinviteId: String, secret: String, passcode: String (optional)preview is cleared and acceptedRecordId is set.
RejectInviteinviteId: StringThe invite is rejected; owned and incoming are reloaded.
RevokeInviterecordId: String, inviteId: StringThe invite is revoked; owned and incoming are reloaded.
RevokeAccessrecordId: String, accountId: StringThe account loses access; access is reloaded for the record.

On iOS the requests are lowerCamelCase cases with labeled fields, such as .acceptInvite(inviteId:secret:passcode:); on Android they are classes of SharingRequest, such as SharingRequest.AcceptInvite(inviteId, secret, passcode), and SharingRequest.LoadInvites is an object; on the web they are functions of SharingRequest, such as SharingRequest.acceptInvite(inviteId, secret, passcode), and SharingRequest.loadInvites is a value. Wrap each request in VaultAction.Share before you dispatch it.

LoadInvites and LoadAccess return up to 100 entries each. A failed request leaves sharing as it was and sets error in the vault state; a successful one clears error. busy is true while a request runs.

let invite = InviteRequest(audience: .open(multiUse: false), expiresAt: nil, passcode: "4821")
try vault.dispatch(action: .share(value: .createInvite(recordId: record.header.id, invite: invite)))

try vault.dispatch(action: .share(value: .previewInvite(linkId: linkId)))
try vault.dispatch(
    action: .share(value: .acceptInvite(inviteId: preview.id, secret: secret, passcode: "4821"))
)

InviteRequest

FieldTypeMeaning
audienceInviteAudienceWho can accept the invite.
expiresAti64 (optional)When the invite expires, seconds since the Unix epoch. Without a value the invite does not expire.
passcodeString (optional)A passcode the recipient must give to accept. The invite carries only a hash of it.

InviteAudience

CaseFieldsMeaning
OpenmultiUse: BoolAnyone with the link and the secret can accept. multiUse asks for an invite that more than one account can accept.
AccountaccountId: StringOnly the named account can accept.

On iOS the cases are .open(multiUse:) and .account(accountId:), on Android InviteAudience.Open(multiUse) and InviteAudience.Account(accountId), and on the web InviteAudience.open(multiUse) and InviteAudience.account(accountId), which build { type: 'OPEN', value: { multiUse } } and { type: 'ACCOUNT', value: { accountId } }.

An account id is the userId of the account's session identity. FolioSDK has no directory that finds an account by email address or name: the recipient reads their own userId from SessionStore and gives it to the inviter through your app or your backend. The account ids the SDK already shows you are the recipients in AccessGrant and the owner of a shared record in header.ownerId.

expiresAt is an Int64 on iOS, a Long on Android and a bigint on the web. It counts seconds, not milliseconds: an invite that expires in a day is BigInt(Math.floor(Date.now() / 1000) + 86_400) on the web. The recipient sends the passcode with AcceptInvite, and Folio checks it against the hash.

SharingView

The sharing field of the vault state.

FieldTypeMeaning
owned[InviteSummary]Invites the account created. Filled by LoadInvites.
incoming[InviteSummary]Invites addressed to the account. Filled by LoadInvites.
access[AccessGrant]Accounts with access to the record of the last LoadAccess.
createdCreatedInviteLink (optional)The invite of the last successful CreateInvite.
previewInviteSummary (optional)The invite of the last successful PreviewInvite.
acceptedRecordIdString (optional)The id of the record the last successful AcceptInvite added to the vault.

created keeps the last created invite until the next CreateInvite replaces it. Keep the secret only as long as you need it to deliver the invite.

FieldTypeMeaning
inviteInviteSummaryThe new invite.
secretStringThe invite's secret, base64-encoded. The recipient needs it to accept.

InviteSummary

FieldTypeMeaning
idStringThe invite id. Use it in AcceptInvite, RejectInvite and RevokeInvite.
recordIdStringThe shared record.
audienceInviteAudienceOpen with multiUse, or Account with the invited accountId.
statusInviteStatusThe invite's status. See below.
challenges[InviteChallengeType]What the recipient must provide. Passcode means the invite asks for a passcode.
linkIdStringThe public link id. Use it in PreviewInvite.
ownerIdStringThe account that created the invite.
expiresAti64 (optional)Expiry, seconds since the Unix epoch. Absent when the invite does not expire.
createdAti64 (optional)Creation time, milliseconds since the Unix epoch. Absent when unknown.
updatedAti64 (optional)Time of the last change, milliseconds since the Unix epoch. Absent when unknown.

expiresAt keeps the seconds of the InviteRequest, while createdAt and updatedAt are in milliseconds. Multiply expiresAt by 1000 before you compare it with the other two.

InviteStatus

StatusMeaning
PendingThe invite can be accepted.
ConsumedThe invite was accepted. A multi-use open invite stays Pending after an accept.
RejectedThe recipient rejected the invite.
RevokedThe owner revoked the invite.

InviteStatus and InviteChallengeType have no payloads. On iOS the cases are .pending, .consumed, .rejected, .revoked and .passcode; on Android InviteStatus.PENDING, InviteStatus.CONSUMED, InviteStatus.REJECTED, InviteStatus.REVOKED and InviteChallengeType.PASSCODE; on the web the strings 'PENDING', 'CONSUMED', 'REJECTED', 'REVOKED' and 'PASSCODE', also available as InviteStatus.PENDING and so on.

let canAccept = preview.status == .pending
let needsPasscode = preview.challenges.contains(.passcode)

AccessGrant

FieldTypeMeaning
recordIdStringThe shared record.
accountIdStringThe account that has access.
grantedAti64 (optional)When access was granted, milliseconds since the Unix epoch. Absent when unknown.

Shared records in the recipient's vault

An accepted record appears in the recipient's records like any other record, with the owner's account in header.ownerId and header.permission set to View. The recipient can read it and write its own settings for it, but cannot update, delete or restore it. A conflict of RemoteRevoked means the account lost access to the record; see Conflicts.

Issued documents

An organization delivers a document it issues to the person as an Account invite. The SDK accepts these invites on its own, once Folio confirms that an invite carries an issued document, and the document appears in FolioDocumentListStore. Your app does not dispatch AcceptInvite for them. See Automatic reception.

The SDK never accepts any other invite on its own. An invite another account sends stays in incoming until your app accepts or rejects it.

Errors

Sharing requests fail with an error in error of the vault state, a VaultErrorView whose details is the VaultError. The cases specific to sharing:

ErrorWhen
ChallengeFailedThe passcode check failed.
InviteExpiredThe invite has expired.
InviteNotPendingThe invite was already consumed, rejected or revoked.
InviteNotFoundNo invite has the given id or link id.
InvalidWrappedKeyThe invite carries no key to accept it with.
NotFoundCreateInvite names a record that is not in the vault.
TransientNotSupportedThe session is transient; sharing needs a signed-in session.

See Vault errors for the full list and the spelling of the cases on each platform, and Types on each platform for i64, Bool and the list types in the tables above.

On this page