How we protect your sanctuary's data, on the page and behind it
Every connection to ARA runs over TLS. The site is served from Firebase Hosting, which redirects plain HTTP to HTTPS at the edge and uses modern cipher suites by default. The app cannot be loaded over plain HTTP at all. HSTS is set to one year with includeSubDomains and preload, so browsers refuse plain-HTTP connections to arbosfolk-hosted subdomains even on first visit.
Persistent data lives in Google Cloud (Firestore, Cloud Storage, Firebase Authentication). All three encrypt at rest using AES-256 by default, with Google-managed keys, as documented in Google's default encryption policy. ARA does not run its own database, file store, or auth service. We rely on Google Cloud's encryption rather than maintaining a custom layer that could weaken the standard.
Firebase carries SOC 2 Type II, ISO 27001, ISO 27017, ISO 27018, and HIPAA certifications. Stripe holds PCI DSS Level 1. Those certifications apply to the infrastructure underneath ARA.
Members of one Sanctuary cannot see another Sanctuary's data. Every read and every write to Firestore is gated by Firestore Security Rules (file: firestore.rules) that check the requester's membership at the moment of the request, enforced at the database layer rather than in the browser. Even with developer tools open and the Firebase SDK in hand, an attacker cannot reach data they do not belong to.
The role model inside a Sanctuary:
When Sanctuaries connect to each other through Mycelium federation, both sides must explicitly accept. One-sided forgery is impossible: federation links can only be mutated through the audited linkSanctuariesBilateral and disconnectFromSanctuary Cloud Functions, never by direct client writes.
The Firestore rules file ends with a catch-all deny. Any path not explicitly allowed is denied by default. Adding a new feature means writing an explicit rule; nothing is permitted because we forgot to lock it.
Some fields are too sensitive to allow direct client writes, even by their owner. ARA enforces this at the rules layer with blocklists that only Cloud Functions (running with Admin SDK credentials) can satisfy. A user who tries to write any of these directly from the browser gets a permission-denied error from Firestore. The fields exist in the database; only the server can mutate them.
plan, subscriptionStatus, stripeCustomerId, subscriptionRenewalDate can only be changed by the Stripe webhook handler.memberLimit, storageLimitGB, nodeLimit, fileSizeLimitMB, rhythmLimit, gardenLimit, seedLimit, isGrantRecipient, manualPlanOverride are server-only.hasUsedTrial, trialStartedAt are server-only, so a Sanctuary cannot self-renew a one-time trial by editing the doc.members, admins, guests, ownerId, inviteCode, scheduledForDeletionAt, connectedSanctuaryIds, ecosystemName are server-only. A Sanctuary admin cannot directly add or remove members, forge a federation link, or rename an ecosystem by writing to the doc.isAngel. Platform operator flag. Bestowed by Cloud Function only; immutable on self-update.sanctuaries. The membership-list field used by every federated-read helper. Cloud Function only; otherwise an attacker could claim membership of any Sanctuary they knew the id of.isDemo. World-readable demo flag. Immutable on self-update so a real user cannot promote their profile into the anonymous-demo view.votes, status, isEscalated. Governance state is mutated only by the castVote and updateProposalGovernance Cloud Functions. Members cannot rewrite the votes array, flip a closed proposal back to active, or self-escalate.signatures, isAgreement, resolutionPayload, councilResolutionsFor, linkedProposalId. Signing and agreement flows go through dedicated Cloud Functions (signScroll, revokeSignatureOnScroll, toggleScrollAgreement) so a Sanctuary member cannot forge a signature attributed to another user.Privileged operations never run directly from the browser. They go through audited Cloud Functions (file: functions/src/index.ts) that re-verify the caller's identity and authority at the moment of the action. The Cloud Function names below appear in the actual codebase:
createSanctuarySecure, joinSanctuaryByInviteCode, leaveSanctuarySecure, kickSanctuaryMember, lookupSanctuaryByInviteCode.linkSanctuariesBilateral, disconnectFromSanctuary, createEcosystemInvite, lookupEcosystemInvite, acceptEcosystemInvite, revokeEcosystemInvite, setEcosystemName.castVote, addPollOption, updateProposalGovernance, generateResolutionScroll.signScroll, revokeSignatureOnScroll, toggleScrollAgreement./api/stripe-webhook), startTrial, expireTrial, updateTrialEndDate, unlinkStripeCustomer.postNotification, signalSanctuaryAdmins, submitContactForm. These exist so a signed-in user cannot directly write a notification doc for another user (a phishing primitive that also triggers a push notification).enterSanctuaryAsAngelSecure, flagPublicDemoSanctuary, refreshPublicDemoData.Why this matters
plan field. The rules layer blocks all three at the database level; the Cloud Functions are the only path that re-verifies the request properly.All payments are processed by Stripe (PCI DSS Level 1). ARA never receives, stores, or transmits a card number, CVC, or bank account number. Stripe's checkout pages, hosted on Stripe's own domain, handle that data.
ARA stores only the references Stripe gives back: the customer ID, the subscription ID, and metadata about which Sanctuary a subscription belongs to. Those references mean nothing outside Stripe's account and cannot be used to charge a card without Stripe's authentication.
Subscription changes (upgrades, cancellations, failed payments) are pushed to ARA through Stripe webhooks. Every webhook is verified with Stripe's official signature scheme before any database write happens (see Webhook verification below). Plan prices are also validated against a server-side allowlist, so a tampered checkout request cannot grant a free upgrade.
Visitors who tap "Riverbend Commons" or visit /demo sign in to ARA through Firebase anonymous authentication. The session is real, but it is pinned to the single public-demo Sanctuary and is denied write access to everything.
The isolation is enforced at three layers:
isAnonymousAuth() helper detects anonymous tokens, and every allow create, allow update, and allow delete rule gates on isRealUser() (which excludes anonymous sessions). Reads are scoped to the demo Sanctuary (via isPublicDemoOf()) and its federation partners.A demo visitor cannot pollute a real Sanctuary's roster, log a Ledger contribution, or write to any document outside the demo's read scope. The anonymous session is throwaway and can be discarded at any time.
ARA receives webhooks from two external systems: Stripe (subscription state changes) and Sentry (error triage). Every incoming webhook is verified before its payload is trusted.
Stripe webhooks use Stripe's official signature scheme via stripe.webhooks.constructEvent(rawBody, signature, secret). Sentry webhooks use HMAC-SHA256 with a shared secret. In both cases the raw request body is hashed and compared against the signature header; if the comparison fails, the request is rejected before any business logic runs.
Why this matters
Storage rules (file: storage.rules) enforce strict limits on what can be uploaded:
image/png, image/jpeg, image/webp, image/gif, and application/pdf are accepted. SVG is explicitly denied because an SVG file can carry an inline <script> that runs when a user opens the direct URL, and Storage serves files from a Google-owned domain (a convincing phishing surface).{collection}/{uid}/{filename}, with the uid in the path identical to the uploader's authentication uid. You cannot write a file into someone else's upload prefix.Every response from Firebase Hosting carries a hardened set of HTTP headers (file: firebase.json):
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadX-Frame-Options: DENY (the app cannot be loaded in a frame)X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(), geolocation=() (the app cannot trigger any of those prompts)Cross-Origin-Opener-Policy: same-origin-allow-popups (lets the Google sign-in popup work without weakening tab isolation)Cross-Origin-Resource-Policy: same-siteA Content Security Policy is set on index.html as a meta tag. It defaults to 'self' and explicitly enumerates the trusted third parties we use (Stripe Checkout, Google Sign-In, Firebase, Sentry, Google Fonts, *.cloudfunctions.net). New origins are listed explicitly; we do not fall back to wildcards. base-uri 'self' stops an injection from rewriting the document base URL to a malicious origin.
Every place ARA renders text one member wrote and another member sees (Scrolls, comments, Council proposals, task descriptions) runs through DOMPurify, the same XSS-prevention library used by GitHub, Slack, and Notion. Inline scripts, dangerous URL schemes (javascript:, data:text/html), and DOM-clobbering tricks are stripped automatically.
External links open with rel="noopener noreferrer" so a malicious destination cannot reach back into your session. Avatar and cover images use a strict URL allowlist (only https: or data:image/) and reject any character that could break out of a CSS context.
We follow GDPR Art. 32 technical and organisational measures, are subject to the EU-US Data Privacy Framework via our subprocessors (Google, Stripe), and offer a Data Processing Agreement to Sanctuaries that need one (request via legal@arbosfolk.com). For the complete list of subprocessors, retention periods, and your data-subject rights, see the Privacy Policy.
Source maps are built but served as sourcemap: 'hidden', so production code cannot be reverse-engineered through the browser for hidden routes or admin emails. The maps remain available server-side for error tracking.
Sentry monitors ARA for runtime errors, performance regressions, and unexpected failures. Errors are triaged automatically; high-severity errors trigger a GitHub issue that is investigated within one business day. Sentry's session replay is recorded only for users who consented to analytics cookies; with consent declined, no replay is captured.
No system is perfectly secure. If we become aware of a confirmed personal-data breach with high risk to individual rights, we notify the Danish Data Protection Authority (Datatilsynet) within 72 hours and affected individuals without undue delay, as required by GDPR Art. 33-34. We commit to honest communication, including a post-incident write-up where appropriate.
If you find a security vulnerability in ARA, please email legal@arbosfolk.com (or security@arbosfolk.com) with:
We acknowledge security reports within two business days. We don't yet operate a paid bug bounty programme, but we credit reporters by name in our changelog where appropriate, and we will never threaten legal action against a researcher acting in good faith.
Please do
For non-urgent security questions, write to us:
Entity
Arbos Folk
CVR 46278895
Address
Byhøjvænget 17
8380 Trige, Denmark
Responsible person
Leif Pettersen
More legal documents
Privacy Policy
What we collect, why, and how long we keep it.
Terms of Service
The agreement you accept when you sign in.
Cookies Policy
Which cookies ARA uses and how to manage them.
Legal Notice
Operator identification and supervisory authority.