Legal · Public surface

Sheet Zaber — Privacy Policy

Operator facts supplied 2026-11-06 by Osayeed Zaber, who owns the service. Nothing here is a template filler: each clause is written from the shipped code and schema.

Last updated
6 November 2026
Applies to
sheet.zaber.tech
Operator
Osayeed Zaber
Contact
support@zaber.tech

01What this covers

Sheet Zaber (sheet.zaber.tech) is a private, invitation-only application that turns one tab of one Google Sheet you own into one REST endpoint. It is an authorized proxy in front of your own Google account, not a store: your rows stay in Google Sheets. This policy says what we record about you, what we never record, and how to get it removed. Browsing these pages records nothing — there is no visitor or analytics table for anonymous traffic.

02Who operates it

Osayeed Zaber operates Sheet Zaber and is the sole data controller. Postal address for statutory requests: Sector 18, Uttara, Dhaka 1230, Bangladesh. Data requests go to support@zaber.tech.

Technically there is one operator account: the owner role, granted only to the single configured owner email at first sign-in, and the only role with platform-wide visibility of data.

03Invitation-only access

Signing in does not create an account. An account exists only after you redeem an invitation code (ZABER-XXXX-XXXX, issued by the owner) during registration, in the same transaction as the account.

  • We store only the SHA-256 hash of a code, never the code. Codes have a use limit (one by default), optional expiry, optional revocation, and an optional owner label — free text that may describe you.
  • A redemption record says which code, which account, when. One Google identity can never open a second account. There are no passwords: Google is the only credential.

04What we store about you

WhatFieldsSource
IdentityGoogle account id (sub), verified email, role, status, last loginthe ID token Google issued
Profiledisplay name, avatar URL (not the image), theme, timezone, dashboard preferencesname and avatar seeded at registration and refreshed on later sign-ins; the theme, timezone and preferences columns exist but the app never writes them, so they stay at defaults
Google connectionconnected email, granted scopes, encrypted refresh token, status, connected/last-used timesyour consent
Your sheetsspreadsheet id, cached title, tab snapshot: numeric tab id, tab title, row count, column countread from Google when you register
Your endpointsname and optional description you type, tab title, header row number, primary-key column name, permitted verbs, enabled flag, hourly limityour configuration
Your keyslabel, SHA-256 hash, short preview, scopes, allowed endpoints, expiry, revocation, last usedgenerated by us, shown once
Request historymethod, path, status, duration, request byte count, error category, and which key/endpoint/account the call resolved toevery /api/v1 call, refusals included
Admin historywhich owner account did which administrative action, to whom, whenthe audit log

05Google sign-in, and the tokens we hold

Sign-in uses the authorization-code flow with PKCE (S256). state is a one-shot nonce bound to a signed HttpOnly cookie, and the ID token is verified server-side — issuer, audience, expiry, nonce, email_verified — before any account is touched; the token itself is read for its claims and discarded.

Consent asks for five scopes: openid, email, profile, .../auth/spreadsheets and .../auth/drive.file. All five are requested on every consent redirect, sign-in included: the app asks for spreadsheet access when it connects your account, because reading and writing your sheets is the product. The Drive scope only covers files you choose in Google's Picker, so the app cannot list or read the rest of your Drive.

Consent uses access_type=offline + prompt=consent, so Google returns a refresh token — that is what lets an endpoint keep serving your sheet on later calls made by other people holding your API key. Storage of the grant:

  • Refresh token — in our database encrypted with AES-256-GCM, random 12-byte IV per encryption, key from a server-only environment secret. Never sent to a browser, never logged.
  • Access token — never persisted: a short in-process cache (at most 50 minutes), gone on restart.
  • Authorization code — memory, for one request.
  • During registration — the grant sits in a signed, AES-encrypted, HttpOnly cookie for at most 10 minutes until the code is redeemed. It grants no access and is not a session.

Sign-in then leaves one first-party cookie: HMAC-SHA-256 signed, HttpOnly, Secure, SameSite=Lax, 7 days, holding only your user id, role and expiry. No advertising, profiling or analytics cookies.

Withdrawing access is self-service. Settings → Connections lists each Google connection and disconnects it behind a typed confirmation (disconnectGoogleAction): the service asks Google to revoke the refresh token first, and a failed revoke still completes the deletion, because the row is what enforces access. Signing out is separate and always available, from the account menu or /auth/logout. Revoking from your side at Google works too: the next call fails and the connection is marked reauth_required instead of being used. Removing our copy is not revoking at Google — do both, and remove the app in your Google account permissions. While the OAuth client remains in Google's Testing state, Google expires refresh tokens after 7 days, so re-consent is expected behaviour.

06Your spreadsheet data is processed, not copied

We read and write your spreadsheets through the Google Sheets API, with your grant, at the moment a request is made. No code path writes cell values or header names to our database: the engine reads the header row live from Google on each request, and the registration preview shows you a header row plus at most ten rows in your own browser and stores nothing from it.

What we keep is structural — spreadsheet id, cached title, tab titles, row and column counts — plus two things you type yourself into an endpoint form: a tab title and a primary-key column name. No request body, response body or cell value is retained server-side.

Consequence: every call transits our server between caller and Google. An API key can therefore read (and, with write scope, change) the sheet behind it.

07API keys

sz_live_ plus 32 random hex characters, shown once and never stored: only its SHA-256 hash and a short preview (sz_live_ + first 4 and last 4 characters). Nobody — you or the operator — can recover it afterwards. Keys can be scoped read or read-write, restricted to specific endpoints, given an expiry, revoked or regenerated at any time.

08Logs

Every /api/v1 call is recorded, including refusals (bad key, unknown endpoint, disabled endpoint, rate-limited). A row holds: method, request path, status code, duration in ms, request body size in bytes, an error category, the key/endpoint/account the call resolved to, and a timestamp. Paths are recorded without query strings, so filter values, search terms and paging parameters are not logged.

Never logged, because no column exists for them: request or response bodies, headers, cookies, API keys, Google tokens, cell values. No IP address is stored anywhere — the client IP is used only as a one-way hashed component of a transient rate-limit counter. The log also keeps the hostname from a request's Origin or Referer header when one is sent, never its path or query, and the account owner can see it in their usage figures.

Administrative actions (disable, re-enable, role change, invitation create/revoke, key revocation, platform settings) write an audit row naming actor, action, subject and non-secret detail — including refused attempts — and audit rows are not deleted when the account they describe is.

History is the point of the service, so it outlives the records it describes. By the operator's decision of 6 November 2026, a request log is not deleted when the endpoint it answered is removed, and not deleted when the account that owns that endpoint is closed. The row keeps a copy of the endpoint name and the account email so that later it can still be answered what happened to an API, how it happened and who used it. What does expire is age: rows older than the configured window (90 days by default) are removed by the prune sweep, and rate counters are pruned with them. The sweep exists as a scheduled job, and until it is running in the deployment rows may live longer than the stated window.

Unexpected internal errors go to the host's log stream; that window is set by the hosting plan rather than by this application,. Those entries carry an error object, not request bodies or cell values.

09Who can see your data, and who else is involved

Cross-account access is prevented structurally, not by a check that could be forgotten: a call always walks key → key owner → endpoint owner → that owner's connection, and no path accepts a user-supplied token, so someone else's endpoint answers exactly like one that does not exist. Row-level security is enabled on every table with no policies, so a leaked database key reads nothing.

The owner account can see the user list, per-account activity and request totals, all request logs, and connection/sheet/endpoint metadata. The operator has no product surface for reading your cell contents and cannot recover your key plaintext — but holds the database and the encryption secrets, which is why this service is invitation-only rather than open.

Third parties, exhaustively: Resend (delivers invitation emails: the address, the message and the code in it, only when the owner invites someone by email), Google (identity verification and the Sheets API), Render (hosts the application) and Supabase (the managed Postgres holding the rows in §4, in Singapore). No analytics, advertising, email-marketing or support-ticket processor is integrated and none is planned. So that the picture is complete rather than reassuring: the operator administers the service from Bangladesh, the rows live in Singapore, and the application runs on Render — which means your metadata crosses borders by design, even though the cell contents themselves never reach us.

10No ads, no sale of data, no third-party analytics — and your choices

This is a fact of the build, not a promise: no analytics or telemetry dependency exists, the only outbound network calls in server code go to Google APIs and, for invitation emails, Resend, there is no marketing email feature, and the Content-Security-Policy restricts scripts and network requests to this origin, so these pages cannot report to a tracker even by accident. Your data is not sold, shared with advertisers or used for profiling.

Self-service today: sign out; disconnect a Google connection (§5); revoke or regenerate a key; remove a registered spreadsheet, which takes its endpoints with it; read your own logs and analytics. Not self-service: editing profile fields (they are re-seeded from Google at sign-in), exporting your data as a file, and closing your account.

Account closure is an owner action. It removes your profile, Google connections, registered spreadsheets, endpoints and keys. It does not remove your request-log rows, and it does not remove the operator's audit history — see §8 for why, and note that the retained log rows carry your account email. Closure cannot be performed on the account that issued invitations, on the operator's own account, or on the only remaining owner.

Email support@zaber.tech to ask what we hold, or to correct or export it. There is no request form, no automated workflow and no promised response deadline: the request reaches one person who holds the database.

11Jurisdiction, children, changes

The operator is established in Dhaka, Bangladesh, and Bangladeshi law governs this policy. Bangladesh has no general personal-data statute in force comparable to the GDPR or CCPA, so this policy claims compliance with no regime; the access, correction and export routes in §10 are the operator's own practice and are honoured on request rather than a statutory entitlement. The service is not directed at children and implements no age verification: admission requires a Google account plus a code the owner chose to hand over.

This policy lives in the repository beside the code it describes and is republished when the code changes what is stored — last updated 6 November 2026. The published page is the only notice of a change; there is no email or in-product notification mechanism. If this page and the schema disagree about what is stored, the schema is right and this page is wrong; tell the operator at support@zaber.tech.