Skip to content

Security

Security at PaloWorks, and its limits.

PaloWorks holds the commercial spine of your business: who your clients are, what you agreed to build, what they signed, and what they owe you. This page describes what we actually do to protect that, and — just as importantly — what we do not yet do. Every control below is traceable to code in the product.

Last updated Oct 4, 2026 · Ferrier Industries LLC

Accounts and sessions

Authentication runs on Better Auth against our own Postgres database. We never see or store a password in a reversible form.

  • Passwords are hashed with scrypt

    Sourcelib/auth.ts, better-auth password hashing

    Each password is hashed with scrypt (N=16384, r=16, p=1, 64-byte output) under a unique 128-bit random salt. scrypt is memory-hard, which makes bulk offline cracking expensive rather than merely slow. Minimum length is eight characters and it is enforced on the server, not just in the browser.

  • Sessions live in the database, so revocation is immediate

    Sourcelib/auth.ts (expiresIn, absoluteSessionDeadline)

    A session goes idle after seven days without use, and it is capped at thirty days from sign-in even if it is used every day. It refreshes when it is more than a day old, never past that thirty-day cap. Because every request validates the session against a database row rather than trusting a self-contained cookie, signing out or resetting a password takes effect at once.

  • A password reset invalidates every existing session

    Sourcelib/auth.ts (resetPasswordTokenExpiresIn, revokeSessionsOnPasswordReset)

    Reset links expire after one hour, and completing a reset revokes all sessions for that account. Someone who resets their password because they think they were compromised actually ends the intruder's access.

  • Sign-in attempts are rate limited

    Sourcelib/auth.ts (rateLimit.customRules), lib/rate-limit.ts

    Ten sign-in attempts per fifteen minutes, twenty sign-ups per hour, and five password-reset or verification-email requests per fifteen minutes, counted per client. Tight enough to make credential stuffing impractical, loose enough that a shared office IP is not locked out.

  • The signing secret must be set in production

    Sourcelib/auth.ts

    The app refuses to boot in production without BETTER_AUTH_SECRET, because a generated-per-deploy secret would silently invalidate every session on each release and mask the misconfiguration.

  • Email verification is sent, and it is not required to sign in

    Sourcelib/auth.ts (requireEmailVerification), invite pages, lib/entitlements.ts (isCompedPro)

    A verification email is sent. Signing in with a password does not require the address to be verified. Accepting a workspace invite does, and a complimentary Pro grant does too. Changing the address on an account requires the new address to confirm before it replaces the old one.

Workspace isolation

Your workspace is the security boundary. Nothing in the app reads or writes a record without proving it belongs to the workspace the signed-in user is in.

  • Every query is scoped to a workspace

    Sourcelib/workspace.ts (getCurrentWorkspace, requireCapability), lib/roles.ts

    The active workspace is resolved from the session on the server, and every database read and write is filtered by that workspace id. Billing, the security policy, team membership, and settings such as branding, templates and invoicing additionally require an owner.

  • Submitted record ids are never trusted

    Sourceapp/(app)/**/*-actions.ts

    A form can be forged, so an id arriving in a request body is re-resolved through its verified parent before anything is written. Posting someone else's invoice id does not reach someone else's invoice.

    The limit of this: Tenant isolation is the application filter: every query is scoped to a workspace id. Postgres row-level security is enabled with no permissive policies, which blocks the Supabase Data API from reading these tables. That is not a per-tenant policy. A bug in the application filter can still cross workspaces. It is a real control, and it is not a wall.

  • Submitted values are validated before they reach the database

    Sourcelib/validation.ts, lib/prisma.ts

    Form input passes through hand-written typed readers that enforce length, format, and range, strip control characters that Postgres rejects or that render as invisible junk on an invoice, and read money strictly as integer cents. Queries are parameterised, including Prisma tagged raw SQL. The app does not build SQL by concatenating strings.

Client-facing share links

Invoices, scopes, intake forms, and contracts are shared with your clients as unguessable links. Your clients do not need an account, which means the link itself is the credential.

  • Tokens carry 160 bits of entropy

    Sourcelib/tokens.ts

    Share tokens are 20 bytes from the operating system's cryptographic random source, encoded URL-safe. Guessing one is not a practical attack at any rate of requests.

  • Shared documents are kept out of search engines

    Sourcenext.config.ts

    Every client document URL — invoice, scope, intake, contract, agreement, delivery, invite, Book me, and the rest of the token pages — is served with X-Robots-Tag: noindex, nofollow, noarchive, so a link forwarded into a crawlable inbox or wiki does not end up in a search index.

  • Links can be regenerated

    Sourcelib/share-links.ts, lib/audit.ts

    Regenerating a share link mints a new token and immediately breaks the old one, and the regeneration is recorded in the workspace audit log. The Book me link shown in Settings is a stored token: rotating it does not sign the studio out, and it also stops the older signature-based Book me snippets.

Payments

Card data never touches our servers. Stripe handles it end to end, and client payments settle into your own Stripe account rather than ours.

  • We never receive card numbers

    Sourceprisma/schema.prisma (Workspace), app/api/stripe/*

    Card entry happens in Stripe's own hosted interface. What we store is a Stripe customer id, a subscription id, and a connected account id — references, not instruments. There is no card number, CVC, or bank account number in our database, and no schema field that could hold one.

  • Client payments go to your Stripe account, not ours

    Sourcelib/stripe-connect.ts

    Invoice payments are charged directly on your connected Stripe account. A plan-based PaloWorks fee comes out of payments clients make online through PaloWorks. Your clients' money is never held in a PaloWorks balance and never passes through our books.

  • Webhooks are signature-verified and replay-safe

    Sourceapp/api/stripe/webhook/route.ts, prisma/schema.prisma (ProcessedStripeEvent)

    Every Stripe webhook is verified against the endpoint signing secret before it is acted on, and each event id is recorded so a retried delivery cannot apply the same change twice.

  • PCI scope

    Sourceapp/api/stripe/*

    Stripe is a PCI DSS Level 1 service provider. Because cardholder data never reaches our systems, PaloWorks itself is outside PCI scope rather than certified within it — an important distinction, and one worth stating plainly.

Transport and browser hardening

Security headers are set on every response by the application rather than left to the host, so they hold even behind a proxy that rewrites headers.

  • HTTPS is enforced for two years, including subdomains

    Sourcenext.config.ts

    Strict-Transport-Security is sent with max-age=63072000, includeSubDomains, and preload, so a browser that has seen the site once will refuse to connect over plain HTTP afterwards.

  • A Content Security Policy restricts where the page can talk

    Sourcelib/content-security-policy.ts, proxy.ts, next.config.ts, lib/chatgpt-ads-origins.ts

    default-src is limited to our own origin. The browser may connect to our own origin, our file-storage endpoint for uploads, PostHog (https://us.i.posthog.com and https://us-assets.i.posthog.com) for page-visit analytics, and Microsoft Clarity (*.clarity.ms) for heatmaps of the public marketing pages. The policy also permits Clarity's tag (www.clarity.ms and scripts.clarity.ms), which our code loads only on the public marketing pages, and Google's ads tag (www.googletagmanager.com, with the scripts it may load from www.google.com and www.googleadservices.com), which our code loads only on the public site of paloworks.com in production, and on a blank page of ours that reports a new workspace. The browser may also connect to Google's ads endpoints (www.google.com, www.googleadservices.com, googleads.g.doubleclick.net, stats.g.doubleclick.net, ad.doubleclick.net, pagead2.googlesyndication.com, and www.googletagmanager.com) for that tag's page views and that one conversion, and may load the stylesheet and fonts of Google's Tag Assistant badge (www.googletagmanager.com, fonts.googleapis.com, fonts.gstatic.com), which appears only while someone is debugging the tag. The tag may load a frame from www.googletagmanager.com. Framing of our pages is forbidden (frame-ancestors 'none' plus X-Frame-Options: DENY) except for three pages: the Book me embed, which studios put on their own sites, and two blank pages, one that reports a Google Ads conversion and one that runs OpenAI's ChatGPT ads measurement, which only our own pages may frame. No other page's policy names OpenAI's hosts: the ChatGPT ads page has a policy of its own, which sandboxes it into an opaque origin (sandbox allow-scripts, also set on the frame) so it cannot read the page that framed it or our cookies, and lets it load OpenAI's measurement script (bzrcdn.openai.com) and send to OpenAI (bzr.openai.com and bzrcdn.openai.com), and nothing else; our code frames it only for a visitor who arrived from one of our ads in ChatGPT. Plugins are blocked with object-src 'none', <base> is locked to our own origin, and forms may submit only to our own origin and Stripe Checkout. Pages you sign in to and client documents get a fresh script nonce on every request, with 'strict-dynamic', so an inline script without that nonce does not run.

    The limit of this: Prerendered public pages (the marketing site, sign-in pages, pricing) are served as static files that cannot carry a per-request nonce, so their script-src still permits 'unsafe-inline'. 'unsafe-eval' is allowed only in local development. Treat our CSP as defence in depth rather than as XSS protection on its own.

  • MIME sniffing, referrers, and device APIs are constrained

    Sourcenext.config.ts

    X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin so full URLs (which contain share tokens) are not leaked to third-party sites, and a Permissions-Policy that denies camera, microphone, and geolocation to the whole application.

  • Abuse limits on public endpoints

    Sourcelib/rate-limit.ts

    Publicly reachable writes — intake submissions, contract signing — are limited to fifteen per ten minutes per client, and expensive operations such as workspace or checkout creation to twenty per hour. State lives in Postgres so the limit holds across serverless instances rather than resetting with each one.

    The limit of this: Public writes fail open if the limiter's database check errors, so an outage does not stop a client paying an invoice. Password sign-in, password reset, and client or contract one-time codes fail closed: if that check errors, the attempt is refused.

Audit and accountability

Once a workspace has more than one person in it, "the invoice was marked paid" stops being a useful answer to "by whom?".

  • Money and access changes are recorded

    Sourcelib/audit.ts

    Invoice creation, deletion, and status changes; scope updates; contract sharing and signing; team member removal; invite creation and revocation; plan changes; payment-account status changes; and share-link regeneration are each written to an append-only workspace log with the actor's id, their email, and a timestamp.

    The limit of this: The log is deliberately narrow — it covers actions that move money or change who can see what, not every read or page view. It is an accountability trail for your team, not a full system log, and it is not tamper-evident.

  • Signed contracts carry their own evidence

    Sourcelib/contract-signing.ts, prisma/schema.prisma (Contract)

    A signature records the typed name, the timestamp, the signer's IP address and browser user agent, and a SHA-256 hash of the exact contract text at the moment of signing. The hash is re-checked on every view, so a contract edited after signature is flagged rather than silently accepted.

What we don’t claim

Read this as the rest of the page. Your security reviewer will ask about every one of these, and the cheapest answer is to have told you first.

  • SOC 2, ISO 27001, or HIPAA

    PaloWorks holds none of these. There is no audit in progress and no report to send you. If your procurement process requires one, we are not yet the right vendor and we would rather say so now.

  • Third-party penetration testing

    No external penetration test or security audit has been performed. Our controls have been reviewed by the people who wrote them, which is worth something but is not an independent assessment.

  • Encryption at rest

    Data is encrypted in transit with TLS on every connection. At rest, we rely on whatever full-disk and volume encryption our hosting and database providers apply by default. We have not independently verified it, we do not manage the keys, and there is no application-level or per-field encryption. So we describe it rather than guarantee it.

  • Single sign-on (SSO)

    Not available. There is no SAML or OIDC single sign-on and no SCIM provisioning. Two-step verification with an authenticator app and passkeys are available, and a workspace owner can require two-step verification for every member.

  • A bug bounty programme

    We do not pay for vulnerability reports. We do read every one of them and we will credit you if you want the credit. See responsible disclosure below.

  • A 24/7 security team

    There is no follow-the-sun on-call rotation. Reports and incidents are handled in business hours by people who can change the code.

  • Tested disaster recovery

    Backups are those our database provider takes automatically. We have not run a documented restore drill, so we cannot quote you a tested recovery time or recovery point objective. Export your data regularly; the app supports it.

  • Password-protected share links

    A share link is a bearer credential: anyone who has the URL can open the document, with no password and no login. That is the point — your clients should not need an account — but it means a forwarded link is a shared document. Regenerate the link to cut off access.

Subprocessors

The third parties that can touch data you put into PaloWorks, and the advertising on our public site, marked visitors only, which sees visitors to paloworks.com and never anything from a workspace. Entries marked conditional are active only where the relevant integration is configured; we list them either way so this page is accurate on every deployment.

  • Vercel Inc.

    In use

    Application hosting, serverless execution, and CDN delivery.

    Data handled
    All data transiting the application, plus request logs containing IP addresses and URLs.
    Processed in
    United States, with global edge delivery
  • Supabase (managed PostgreSQL)

    In use

    Primary database.

    Data handled
    Everything the product stores: accounts, workspaces, clients, projects, scopes, contracts, invoices, payments, time entries, and audit records.
    Processed in
    United States
  • Stripe, Inc.

    In use

    Subscription billing, and Stripe Connect payment processing and payouts for your client invoices.

    Data handled
    Cardholder data (which it collects directly, never through us), billing contact details, payout and identity information you provide during Connect onboarding, and invoice amounts.
    Processed in
    United States and Ireland
  • Resend (Plus Five Five, Inc.)

    Conditional

    Transactional email: invoice and scope share links, reminders, team invitations, password resets, and verification emails.

    Data handled
    Recipient email addresses and the contents of the messages we send.
    Processed in
    United States

    Active when an email API key is configured. Without one, emails are written to the server log instead of being sent, and no data leaves our infrastructure.

  • PostHog, Inc.

    Conditional

    Product analytics — which features get used, and where people get stuck — and session replays of the public marketing pages.

    Data handled
    From our servers: event names, a workspace id (a user id for a sign-up), record ids, and on invoice events the amount in cents. From the browser: the page path with ids, share tokens and the query string removed, the referring site, campaign tags, time on page and scroll depth, the browser's user agent, language, time zone and screen size, which links, buttons and form fields are used on the public marketing pages, and a random visitor id held only in the open page's memory. On the public marketing pages only: replays of what the page shows and how the pointer moves, clicks and scrolls, with what is typed into a form field, any search text shown back on the page, and the text, links and images outside the page's own content masked in the browser, and the address cut to its path; replays never record the signed-in product, the sign-in and sign-up pages, or a share link. After a click on one of our ads in ChatGPT, whether that ad's measurement reached OpenAI. Nothing is stored on the device: no cookie, no local or session storage. A signed-in person is tied to their user id, plan, and role. No client names, email addresses, document text, or the private part of a share link.
    Processed in
    United States

    Active when an analytics key is configured. Without one, events are discarded before any network call is made.

  • Microsoft Corporation (Clarity)

    In use

    Heatmaps and session replays of the public marketing pages — where visitors click, scroll, and stop reading.

    Data handled
    From the browser, on the public marketing pages only: what the page shows, pointer movement, clicks and scrolling, the page's full address with its query string, the referring site, and the browser's user agent, language and screen size. Every form field, and any search text shown back on the page, is masked in the browser; the free tools and the demo, and any page whose address carries more than campaign tags, ad click ids or template filters, are not recorded. No cookie and nothing in local or session storage. Never loaded in the signed-in product, on the sign-in and sign-up pages, or on a share link, so it never sees client names, document text, or a share link.
    Processed in
    United States
  • Google Ads (Google LLC)

    In useVisitors only

    Advertising measurement. Ties an ad click to a workspace created afterwards.

    Data handled
    On the public site, the sign-up page included: Google's own cookies and local storage on the device, including Google's click identifier after an ad click; the address of the page the visitor arrives on with the query string cut to campaign tags and that identifier, the address the visitor came from without its query string, and the page title, and after a move between public pages without a reload, each new address as Google's tag reads it. Once the identifier is stored, no user data may be used for advertising, so no email address or phone number found on the page is sent. One workspace conversion, sent from a blank page of ours that the next page of the site or the workspace opened within half an hour loads in the background: the identifier, the title PaloWorks, a random one-time number so it is counted once, and the address of the page it was opened from, without its query string, with Google's user-data signal on, which the identifier needs. The Google Ads account also counts a visit to the sign-up page as a conversion. No document or client name. It does not load on the sign-in pages, inside a workspace, on a client's link, or on an invitation's sign-up page, and opening a public page from any of those does not send Google their address; moving into a workspace reloads the page without it, though Google may already have that page's address, without its query string or title. Advertising personalisation is off.
    Processed in
    United States
  • OpenAI (ChatGPT Ads)

    In useVisitors only

    Advertising measurement and personalisation for our ads in ChatGPT. Ties an ad click to a workspace created afterwards; OpenAI may also use that, on its own account, to personalise the ads the clicking ChatGPT account is shown.

    Data handled
    Only for a visitor who arrives from one of our ads in ChatGPT, whose link carries OpenAI's reference for the click, which the browser keeps in a first-party cookie; nobody else's browser loads OpenAI's code, and a browser that has opted out (Global Privacy Control, or Your privacy choices on /privacy) never does. The code runs only in a blank page of ours, loaded in the background and sandboxed by the browser, so it cannot read the page the visitor is on, our cookies or storage, and cannot store anything on the device itself; OpenAI's servers may set or read a cookie for their own address when the browser sends them the event, where the browser allows that. It runs on the page the ad opens, if that is one of our product pages, and, for a workspace created within 30 days, on the next page of the site or the workspace opened within half an hour, until OpenAI has received it. The click reference is kept for those 30 days and cleared once the workspace is reported. Sent to OpenAI: the event (the ad's page was opened, or a workspace was created) and its time, the click reference, the blank page's address, a random one-time number so a workspace is counted once, and the code's own start-up and diagnostic notes, with the browser's network address and user agent as with any request. No email address, name, document, anything typed, or address of the page the visitor is on.
    Processed in
    United States
  • Google LLC

    Conditional

    Optional "Sign in with Google" authentication.

    Data handled
    Your email address and name, received from Google when you choose to use it.
    Processed in
    United States

    Active only where Google sign-in is configured, and only for accounts that choose it. Email-and-password accounts never touch Google.

  • OpenAI

    Conditional

    AI-assisted drafting — suggesting scope deliverables, pricing anchored on your own past work, and draft client messages.

    Data handled
    Only the workspace content needed for the specific request: project and scope text, intake answers, and invoice amounts. Pricing history is sent with client names stripped. For logo background cleanup, only the selected logo is sent, and the preview requires your approval before replacing your saved logo. No AI request is made unless you trigger one.
    Processed in
    United States

    Active only where an OpenAI key is configured and on plans that include the AI features. Without a key the AI layer reports itself as unavailable and no request leaves our infrastructure. AI suggestions are never written to your records automatically — a person has to apply one.

  • Amazon Web Services, Inc.

    Conditional

    Private object storage for files attached to a job.

    Data handled
    The file bytes you upload — proofs, signed PDFs, receipts — and the object key. Objects are not public. A download is a short-lived signed URL.
    Processed in
    United States (us-east-1)

    Active when a storage bucket is configured. Without one the product refuses uploads and this processor receives nothing.

When we add or replace a subprocessor, this list changes, and if the change is material we tell you by email or in the product. See the Data Processing Addendum for your right to object.

What we keep, and for how long

Described from the database’s actual deletion rules, not from intent.

DataRetentionWhat deletion removes
AccountYour email address, name, scrypt password hash, and any linked Google account.For as long as your account exists.Deleting your workspace signs you out and ends your access. You can close the account itself from Settings → Account → Close account; after a short wait in which you can cancel, we remove the account record and its linked sign-in methods.
SessionsSession tokens, plus the IP address and browser recorded when each was issued.Idle sessions end after seven days. No session lasts more than thirty days from sign-in, even if it is used every day. Removed immediately on sign-out, on password reset, and on workspace deletion.Removed immediately.
Workspace business recordsClients, projects, intake responses, scopes and deliverables, change orders, revisions, tasks, comments, attachments, time entries, invoices and their line items, recurring invoices, payment records, and your project and contract templates.For as long as the workspace exists.Deleted immediately and irreversibly when the workspace is deleted, by database cascade. A deleted project waits in Settings → Recently deleted for 30 days before it is purged (a project deleted before the signed-records confirmation existed and holding signed records waits until an owner restores it or deletes it forever), but deleting a whole workspace is permanent, so export first.
Signed contracts and signature evidenceContract text, the signer's typed name, signature timestamp, signer IP address and browser, and the SHA-256 hash of the signed text.For as long as the project the contract belongs to exists.Deleted with the workspace, or with a project once it leaves Recently deleted — 30 days after the project is deleted, or sooner if an owner deletes it forever. Deleting a workspace or a project, or deleting a project or a client forever, first lists each signed contract and agreement and each approved scope and change order — with a download, or, for one already in the trash, a note to restore it to download it — and does not go ahead until you confirm you have the copies you need. A project deleted before this confirmation existed that holds any of them — in the earlier trash or in Recently deleted — is not deleted automatically: it stays until an owner restores it or deletes it forever. Closing your account counts the signed records in every workspace you own, and is not scheduled until you confirm you have the copies you need; the workspaces are deleted at the end of its grace period, unless something in them was signed after you asked, or a workspace that became yours after you asked holds signed records: then nothing is deleted, the closure is cancelled, and we email you why. Once deleted, PaloWorks keeps no copy and cannot recover them.
Audit logAction name, the record it affected, the acting user's id and email, and a timestamp.For as long as the workspace exists.Cleared when the workspace is deleted, with one deliberate exception described below.
Deletion tombstoneA single row recording that a workspace was deleted, directly or because its owner closed their account: the workspace id, the id of the user who confirmed it, the timestamp, and, for a direct workspace deletion, how many clients, projects, and invoices went with it and how many signed records the owner confirmed having copies of.Kept indefinitely. It is how we answer "what happened to my data?" months later, and how we can show a deletion was authorised.Not removed by workspace deletion. It contains no names, email addresses, project contents, contract text, or financial detail — only ids and counts. Email us and we will remove it on request.
AI prompts and suggestionsThe workspace content sent to the model for a request you triggered, and the suggestion it returned.The prompt we sent and the response that came back are kept with a record of the request for 30 days, long enough to finish and review a draft, and then the text is erased. The record itself (the kind of request, whether it succeeded, and when) is deleted after a year. A suggestion becomes an ordinary record in your workspace only when you choose to apply it. We also count AI calls, for your plan's usage limit.Deleting your workspace or closing your account deletes these records with it. Once a request has been sent to our AI subprocessor, its own handling is governed by its terms rather than by ours.
Payment and billing data held by StripeCards, charges, payouts, and the identity information Stripe collects for Connect onboarding.Set by Stripe and by financial record-keeping law, not by us — typically several years. We hold only the Stripe identifiers, which we delete with your workspace.Deleting your PaloWorks workspace does not delete anything in Stripe. Close or delete your Stripe account through Stripe.
Operational logsHosting request logs and error traces, containing IP addresses, URLs, and timestamps.Retained by our hosting provider on its own schedule, typically around 30 days.Not individually deletable — these expire on the provider's rolling schedule rather than on request.
Rate-limit countersA hashed key derived from an IP address and a request count; per-workspace counts of a few actions, such as product-update emails; and a daily count of which of our signing keys checked an unsubscribe or booking link, with the workspace for a booking link.IP counters: the length of their window, up to one hour. Per-workspace counts: up to a month. Signing-key counts: about 120 days, so a retired key can be confirmed unused. Each is swept away once it expires.Expires on its own; nothing to delete.

Responsible disclosure

Found something? Tell us before you tell anyone else, and we will treat you well.

Email

security@paloworks.com

A person who works on the code reads every report. There is no autoresponder.

What we promise you

  • A person who works on the code reads every report and answers it by email.
  • We will tell you how serious we judge it to be and what we intend to do about it.
  • We will not pursue legal action over good-faith research that stays within the boundaries below, and we will not ask your hosting provider to act against you.
  • We will credit you publicly when the fix ships, if you want to be credited.

In scope

  • The PaloWorks web application and its API.
  • Authentication, session handling, and workspace isolation.
  • Client-facing share links for invoices, scopes, intake forms, and contracts.

Out of scope

  • Denial of service, volumetric testing, or anything that degrades the service for other people.
  • Social engineering of our team, our customers, or their clients.
  • Findings against our subprocessors' own systems — report those to Stripe, Vercel, Supabase, Amazon Web Services, or Resend directly.
  • Reports produced solely by an automated scanner with no demonstrated impact.
  • Missing headers or configuration hardening with no exploitable consequence.

Ground rules

  • Use only accounts and data you own. Never access, modify, or retain another customer's data — if you stumble into it, stop and tell us what you saw.
  • Give us a reasonable window to fix an issue before you publish it.

Still reviewing us?

Questions this page does not answer?

Email security@paloworks.com. See also our Privacy Policy, Data Processing Addendum, and Terms of Service.