arrow_back Back to HomeTrust

Security & Privacy

How we protect your sanctuary's data, on the page and behind it

ARA holds the kind of information communities don't share lightly: rotas, votes, finances, conversations between members. This page describes the security posture we actually ship, not aspirational claims. Every measure below is implemented in the codebase that runs the platform you sign in to.
01

Encryption in transit and at rest

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.

02

Per-sanctuary access

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:

  • Member. Read access to the Sanctuary's content. Write access scoped to their own work (their own task notes, their own RSVPs, their own pending Ledger entries, their own Council vote).
  • Steward (Elder / admin). Can manage roster, approve Ledger contributions, edit Sanctuary metadata. Cannot modify privileged fields (see below).
  • Owner. Steward powers plus the ability to delete the Sanctuary.
  • Angel (platform operator). Can read across Sanctuaries for support, with every action auditable.
  • Anonymous visitor. Demo mode only. Read access scoped to the public-demo Sanctuary and to public surfaces (federation graph, public Scrolls). No write access of any kind.

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.

03

Privileged-field gating

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.

On Sanctuary docs

  • Subscription state. plan, subscriptionStatus, stripeCustomerId, subscriptionRenewalDate can only be changed by the Stripe webhook handler.
  • Plan limits. memberLimit, storageLimitGB, nodeLimit, fileSizeLimitMB, rhythmLimit, gardenLimit, seedLimit, isGrantRecipient, manualPlanOverride are server-only.
  • Trial state. hasUsedTrial, trialStartedAt are server-only, so a Sanctuary cannot self-renew a one-time trial by editing the doc.
  • Membership. 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.

On user docs

  • 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.

On Council proposals

  • 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.

On Library Scrolls

  • 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.
  • Scroll edits and deletions are additionally gated on the scroll's own permission rank (owner, architect, caretaker), mirroring the in-app UI checks exactly.
04

Server-only operations

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:

  • Sanctuary lifecycle. createSanctuarySecure, joinSanctuaryByInviteCode, leaveSanctuarySecure, kickSanctuaryMember, lookupSanctuaryByInviteCode.
  • Federation (Mycelium and Ecosystems). linkSanctuariesBilateral, disconnectFromSanctuary, createEcosystemInvite, lookupEcosystemInvite, acceptEcosystemInvite, revokeEcosystemInvite, setEcosystemName.
  • Governance. castVote, addPollOption, updateProposalGovernance, generateResolutionScroll.
  • Library agreements. signScroll, revokeSignatureOnScroll, toggleScrollAgreement.
  • Billing. The Stripe webhook handler (mounted at /api/stripe-webhook), startTrial, expireTrial, updateTrialEndDate, unlinkStripeCustomer.
  • Cross-user delivery. 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).
  • Angel / support. enterSanctuaryAsAngelSecure, flagPublicDemoSanctuary, refreshPublicDemoData.

Why this matters

Without server-only routing, a Sanctuary admin could forge a federation edge, promote themselves to Angel by editing their own user doc, or upgrade their plan to Ecosystem by flipping the 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.
05

Payments and Stripe

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.

06

Demo mode isolation

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:

  • UI. Action buttons are disabled or blocked behind a "this is the demo" modal.
  • Firestore rules. The 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.
  • Storage rules. Anonymous uploads are denied; only signed-in users can write to Storage paths.

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.

07

Webhook verification

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

Without signature verification, anyone who learned a webhook URL could forge a "subscription paid" event and upgrade their Sanctuary to Ecosystem tier for free. The signature check makes the URL safe to publish.
08

File uploads

Storage rules (file: storage.rules) enforce strict limits on what can be uploaded:

  • MIME whitelist. Only 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).
  • Per-user path ownership. Uploads must match {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.
  • Size caps. 500 MB per file for Scroll, Garden, and Task attachments; 10 MB for avatars. Tighter per-tier caps are enforced client-side ahead of upload to save bandwidth.
  • Catch-all deny. Any path not matched by an explicit allow rule is denied.
09

Transport headers and Content Security Policy

Every response from Firebase Hosting carries a hardened set of HTTP headers (file: firebase.json):

  • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • X-Frame-Options: DENY (the app cannot be loaded in a frame)
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-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-site

A 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.

10

User content sanitisation

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.

11

Account control

  • Download your data as a JSON bundle from Settings → Account & Privacy → Download My Data (GDPR Art. 15 / Art. 20).
  • Delete your account from Settings → Account & Privacy → Delete My Account (GDPR Art. 17). Authorship on shared Sanctuary content is anonymised; the content itself stays in place because the Sanctuary jointly controls it.
  • Cookie and analytics consent can be revoked at any time from Settings → Account & Privacy → Cookie Preferences, or by tapping Cookies in the footer. No analytics or error-tracking SDK loads until you consent (see Cookies Policy).
12

Privacy compliance

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.

13

Monitoring and breach response

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.

14

What we deliberately don't do

  • We don't sell your data. We don't share it with advertisers.
  • We don't train AI models on your Sanctuary content.
  • We don't run analytics or session replay without your consent.
  • We don't use automated decision-making with legal effects on you.
  • We don't allow client-side writes to fields that affect billing, governance, federation, or ranking. Everything goes through audited server-side functions.
  • We don't serve source maps to browsers.
  • We don't accept SVG uploads, because they can carry executable scripts.
  • We don't skip rule reviews. Every new feature that writes to Firestore goes through an explicit rules review before it ships.
15

Reporting a security issue

If you find a security vulnerability in ARA, please email legal@arbosfolk.com (or security@arbosfolk.com) with:

  • A description of the issue and where it lives (URL, endpoint, screen).
  • Steps to reproduce.
  • The impact you believe it has.
  • Your name and a way to reach you for follow-up.

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

Test against your own account, your own Sanctuary, or the demo Sanctuary (Riverbend Commons). Stop at proof-of-concept; do not exfiltrate other users' data. Report privately first; do not publish before we have had a chance to fix.
16

Contact

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