Home › Help › Account, sign-in, and permissions
Account, sign-in, and permissions
ShootCal does not make you create a separate username or password. You sign in with your existing Google account, and Google itself handles the login. ShootCal never sees your Google password.
When you sign in, ShootCal asks Google for permission to do specific things on your behalf, like read and create calendar events, save clients to Google Contacts, and send confirmation emails from your own Gmail. You see exactly what you are agreeing to on Google's consent screen, and you can decline.
A few extra features ask for permission only the first time you use them, not at sign-in. Tasks and the "find client emails from my Google contacts" scan are two examples. This keeps the normal sign-in simple and avoids an extra warning screen for features you may never turn on.
While those few features are still being reviewed by Google, turning one on may show a "this app is not verified by Google" notice. That is a normal step for a newer app and does not mean anything is wrong. You can always remove ShootCal's access to your Google account at any time from your Google Account security page, with no help from us needed.
Sign-in is Google OAuth 2.0, not a ShootCal password
ShootCal has no password of its own. Identity comes entirely from Google.
On the web app, the browser is redirected to Google's consent screen, then Google redirects back to ShootCal once you approve. The flow is the standard authorization-code flow with PKCE: the server generates a random one-time state value plus a PKCE verifier, stashes both in a short-lived (10 minute), encrypted, HttpOnly, Secure, SameSite=Lax cookie, and sends a SHA-256 challenge to Google. On the way back it validates state, exchanges the code, and reads your identity from the signed id_token Google returns directly to the server over TLS. The token is never accepted from the browser; because it arrives straight from Google's token endpoint over a connection the server itself opened, its signature is trusted, and as defense in depth the server still confirms the token was issued for ShootCal (its audience) and by accounts.google.com before trusting it.
On the native apps (iPhone, iPad, Mac), sign-in uses Google's official GoogleSignIn SDK with ShootCal's own iOS OAuth client id. The SDK presents Google's screen, and on a later launch it can silently restore a prior session so you do not have to sign in again every time.
The exact scopes requested at sign-in, and why each is needed
The web and native sign-ins request overlapping but not identical permission sets. They share the calendar, contacts, and gmail.send scopes, but the web sign-in also asks for basic identity (openid, email, profile) and a Drive scope for the contract archive, while the native sign-in has neither (the GoogleSignIn SDK supplies your identity, and the Drive feature is web-only). At a normal sign-in ShootCal requests:
- openid, email, profile (web), basic identity: your Google account id, email, and name/photo. This is what tells ShootCal who you are. (On native, identity comes from the GoogleSignIn SDK, so these are not in the native scope list.)
- calendar.events, create and edit your sessions/bookings as calendar events.
- calendar.readonly, list your calendars and read existing events.
- contacts, the client manager, which reads and writes Google Contacts so clients sync.
- gmail.send, send the photographer's OWN notification and confirmation emails from their connected Gmail. This is send-only: it cannot read the mailbox. If you decline it, sends fail gracefully and the underlying booking is still saved.
- drive.file (web only), used by the signed-contract archive. This scope only ever lets the app see or manage files it created itself, so it can only ever reach ShootCal's own "ShootCal Contracts" folder. It cannot browse the rest of your Drive.
All of these are Google-classified as "sensitive," not "restricted" (there is no full-mailbox read), so they need consent-screen verification but not a paid third-party security audit.
Incremental consent: Tasks and read-only "Other contacts" are asked on first use, not at sign-in
Two scopes are deliberately kept OUT of the sign-in request and asked for only when you first use the feature that needs them:
- Tasks, for the Tasks / follow-ups feature.
- Other contacts (read-only), read-only access to your auto-saved "Other contacts" (people you have emailed), used by the optional "find client emails from my Google contacts" step of the calendar scan.
Why split them out? These two scopes are still pending Google verification, so requesting them at sign-in would make the very first sign-in hit the "unverified app" warning. Keeping them incremental means a normal sign-in stays on already-verified scopes and never triggers that warning.
On the web, connecting one of these features re-runs Google consent and adds only the new scope. You must already be signed in. The request explicitly forces Google to show the consent screen even to a returning user, and tells Google to keep all of your existing grants so the resulting token carries both the old and new permissions.
On native, the app asks the SDK to add just the new scope, which presents consent WITHOUT a full sign-out, so your existing Calendar grant is never lost. Tasks additionally has a hard guard: it is the only place the Tasks scope is requested and it stays disabled in the released app while verification is pending.
Note: gmail.send and contacts were also incremental historically, but they were later moved into the sign-in bundle because they are Google-verified and don't warn. Only the two still-unverified scopes remain strictly incremental.
What the "unverified app" notice actually means
The notice is Google's standard interstitial for an OAuth client whose requested scope has not yet completed Google's consent-screen verification. In ShootCal's case the entire architecture above exists to keep that notice away from normal sign-in: every Google-verified scope is granted at sign-in, and the only scopes that can trigger the warning (Tasks and read-only "Other contacts") are gated behind an explicit, opt-in "first use" prompt, which the app frames with its own beta notice before sending you to Google.
When the web app loads, the server tells it whether you currently hold each of the two incremental scopes, so the UI knows whether to show a "Connect Google Tasks" prompt with the beta/warning context rather than silently failing.
Where the data actually lives
Web: After a successful sign-in, your Google tokens are stored server-side in a SQLite database on ShootCal's server, kept outside the webroot. The refresh token and access token are encrypted at rest with libsodium's secretbox (XSalsa20-Poly1305). Your browser holds ONLY an opaque random session token in an HttpOnly/Secure/SameSite=Lax cookie; the database stores only a one-way hash of it, so a database leak yields no usable cookies. Your identity is derived solely from that server-side session row, never from anything the browser sends, which is the guarantee that one user can never see another user's calendar. Sessions slide on each request and default to a 90-day lifetime.
Native: OAuth tokens are held by Google's GoogleSignIn SDK, which stores them in the device Keychain; the app's own small Keychain wrapper stores separate web-integration tokens (session/push tokens) as generic-password items, explicitly NOT iCloud-synced. The native app keeps a live snapshot of your granted scopes so the UI can show exactly which permissions you have granted.
The access token is short-lived; the app refreshes it from the stored refresh token as needed, on both the server and natively.
Revoking access and signing out
There are two distinct actions:
Signing out ends your ShootCal session but does not revoke Google's grant. On web, signing out deletes the server session row and clears the cookie. On native, it clears the local session and purges cached event files and the saved calendar selection for a clean reset.
Revoking ShootCal's access to your Google account is done on Google's side, not inside ShootCal: visit your Google Account, then Security, then "Third-party apps with account access" (myaccount.google.com/permissions) and remove ShootCal. ShootCal is built to handle this gracefully: when a Google call later fails because the grant was revoked, the native app clears the dead session and re-shows the sign-in prompt (a light reset that keeps your cached events so re-consenting is seamless). A non-network token-refresh failure is treated the same way.
FAQ
Does ShootCal ever see or store my Google password?
No. Sign-in is Google OAuth 2.0. Google handles authentication on its own pages and returns only tokens and a signed id_token. ShootCal stores OAuth tokens (encrypted on the web backend, in the Keychain on native), never a password. There is no ShootCal password at all.
Can the gmail.send scope read my email?
No. The gmail.send scope is send-only and cannot read the mailbox. It is used solely to send the photographer's own notification and confirmation emails from their connected Gmail. None of ShootCal's scopes include full-mailbox read; all requested scopes are Google-classified 'sensitive,' not 'restricted.'
Why did I get an 'unverified app' warning when I turned on Tasks but not when I first signed in?
Because the Tasks scope and the read-only "Other contacts" scope are still pending Google verification, so they are deliberately excluded from the sign-in request. They are requested incrementally on first use, when you actually turn one of those features on. Sign-in only uses already-verified scopes, so it stays warning-free; the warning can only appear when you opt into one of those two beta features.
If I grant an extra scope later, do I lose my existing Calendar permission?
No. On web, the incremental consent request tells Google to keep all prior grants, so the new token carries both old and new scopes. On native, the SDK adds the new scope without a full sign-out, so your existing Calendar grant is untouched. The native app keeps a live snapshot of your granted scopes so the UI reflects exactly what you hold.
Does the drive.file scope let ShootCal read my whole Google Drive?
No. The Drive scope (web only, used for the signed-contract archive) limits the app to files it created itself. It can only ever reach ShootCal's own 'ShootCal Contracts' folder. It cannot browse or read the rest of your Drive.
How is my Google refresh token protected on the server?
It is encrypted at rest with libsodium's secretbox (XSalsa20-Poly1305) and stored in a SQLite database kept outside the webroot. Your browser never holds the Google token; it holds only an opaque session cookie, and the database stores only a one-way hash of it, not the value itself.
How do I completely revoke ShootCal's access, and what happens in the app afterward?
Signing out inside ShootCal only ends your session. To revoke the OAuth grant, go to your Google Account security page (myaccount.google.com/permissions) and remove ShootCal. Afterward, the next Google call fails, the native app clears the session and re-shows the sign-in prompt while keeping cached events so re-consenting is seamless.
What stops one ShootCal web user from seeing another user's calendar?
Server-side session isolation. Your identity is derived solely from the server-side session row keyed by the cookie's hash; the browser never sends a user id or calendar owner that the server trusts. Every request resolves the user from that row, so a user can only ever reach their own connected Google data.
What's the difference between a full sign-out and the "expired credentials" path on native?
Signing out is a full reset: it clears the session and purges cached event files plus the saved calendar selection. The expired-credentials path (fired when a Google call fails after a revoke or a non-network refresh failure) is a light reset: it clears the dead session so the sign-in prompt reappears but deliberately keeps cached events and calendar selection, so re-consenting after a revoke or password change doesn't force a full re-setup.