Skip to Content
Authentication

Authentication

IndicaView has optional authentication. There is a real Supabase sign-in and a My Account area — but signing in is not required to use anything.

This is a change from earlier versions of these docs, which stated there was no authentication at all. That was true when they were written and is no longer true.

How optional is it?

A single constant decides:

AUTH_MODE = 'optional' // default

It lives in app/js/auth/navigation.js. In the default mode, workspace routes, restored views and history all work for guests.

What 'required' mode actually changes — and what it does not

Changing the constant to ‘required’ makes navigation wait for a session to resolve before it will hand you a workspace, restored view or history entry. That is a navigation policy, not security.

It does not protect anything downloadable. Every static asset the browser fetches is still fetchable by anyone who can reach the origin. Data protection comes from row-level security and server-side authorization, not from this flag — see Row-level security.

Where the routes live

Authentication is not a separate site. It is a workspace, reached by query parameter on the main page:

RoutePurpose
index.html?workspace=auth&flow=loginSign in
index.html?workspace=auth&flow=signupCreate an account
index.html?workspace=auth&flow=confirmationEmail confirmation callback
index.html?workspace=auth&flow=resetRequest a password reset
index.html?workspace=auth&flow=recoverySet a new password
index.html?workspace=account&section=profileMy Account → Profile
index.html?workspace=account&section=securityMy Account → Security
Why the legacy pages still exist

account.html and callback.mjs are kept so that bookmarks and already-sent emails keep working. They forward only an allow-list of query parameters, plus the untouched fragment, to the main page. The single Supabase client consumes the tokens and then cleans the URL.

That means existing redirect allowlists in the Supabase dashboard remain valid — you do not have to reconfigure every emailed link when the workspace layout changed.

Flows

FlowSupportedNotes
Sign in / sign upYesEmail and password
Email confirmationYesResend and status display are implemented; whether confirmation is enforced is a dashboard setting, not code
Password reset → recoveryYesRecovery takes precedence until completed or cancelled
Change emailYesPending and current addresses are distinguished
Sign outYesLocal scope; never clears optimizer or theme storage
OAuth providersNoEmail/password only — no OAuth, OTP or SSO anywhere in the client
MFA / 2FANoNot present in the client
“Remember me”NoSession persistence is always on, with no user control
Delete accountNoNot offered anywhere
CAPTCHANoThis client does not submit CAPTCHA tokens
Session handling, and what a stale session cannot do

One service.mjs instance and one client exist per document. Forms, the shared header and the account editor all subscribe to it.

  • Sessions persist and refresh across reloads; logout broadcasts to other tabs.
  • Recovery uses a user-ID-only marker in session storage, so a reload inside a recovery flow is recognised — but an invalid recovery link cannot borrow an ordinary session.
  • Private content is hidden while the session is still loading, rather than flashing.
  • A UUID change or a logout destroys account drafts. Switching workspace or account tab preserves them.
  • Identity is UUID-based. Email may be null, and email-dependent controls are hidden when it is — rather than rendered broken.

Profile metadata is rendered as text and never authorizes anything.

My Account

Two tabs, not a separate app section:

Profile — display name and contact phone.

Security — request an email change, request a password reset, sign out.

Name is a trimmed string of at most 80 characters, taken from signup metadata. Absent, malformed, blank or oversized values become null rather than failing the signup. Phone is 6–15 digits and accepts +, spaces, brackets and hyphens. It starts null, and clearing an optional value stores null explicitly.

Where a profile lives, and who can write to it

Profiles live in a Supabase table holding only: UUID, nullable display name, nullable contact phone, and system timestamps.

Rows are created by a database trigger on signup, never by the browser. The client issues one kind of write — an ownership-filtered UPDATE — and never an upsert or insert. A missing profile therefore produces a retryable service error rather than silently recreating a row.

Email changes stay Auth operations. Passwords and the contact phone are never stored in the profile table.

Row-level security

This is the part that actually protects data.

  • SELECT and UPDATE are limited to the row where (select auth.uid()) = id.
  • Ownership is required in both UPDATE conditions.
  • Authenticated browser grants are SELECT plus column-level UPDATE of name and phone only.
  • Browser INSERT, DELETE, owner changes and timestamp writes are all denied.
  • Trigger functions live in a non-exposed account_private schema with fixed search paths and revoked browser access.
Why 'required' mode is a routing decision, not a lock

A static workspace is, by definition, downloadable. Anyone who can reach the origin can fetch the JavaScript, the HTML and the vendored bundles whether or not a session exists.

So the honest framing is: AUTH_MODE controls who the workspace admits to the door. Row-level security and server-side authorization control whose data they may read. Only the second one is a security boundary.

Configuration

Two values, both public:

SUPABASE_URL= SUPABASE_PUBLISHABLE_KEY=

Put them in an ignored .env.local, then:

PowerShell
npm ci
npm run auth:configure

That writes an ignored app/auth-config.json, which must be included in a deployed app. An empty template is tracked at app/auth-config.example.json. Environment variables take precedence over the file.

Dashboard setup

  1. Authentication → URL Configuration — keep existing redirects and add the three account.html?flow= callback URLs (confirmation, recovery, email-change).
  2. Keep the Confirm signup, Reset password and Change email address templates using {{ .ConfirmationURL }}. The client uses implicit flow; a custom server callback or token_hash template is not implemented.
  3. Email/password enabled, confirm email enabled, minimum password length at least 8.
  4. Validate SMTP. Supabase’s default email service restricts recipients and is for testing only — configure custom SMTP for real delivery and disable provider link tracking.
  5. For production, set the correct HTTPS Site URL and allowlist the deployed callback URLs.

On a fresh checkout, auth is off

This matters, because the generated config file is git-ignored.

Verifying it

npm run test:auth # unit: config, service, profile, navigation npm run test:auth:browser # browser flows, real Edge by default

Browser tests use installed Microsoft Edge; set AUTH_BROWSER_CHANNEL=chrome for Chrome. They use the real vendored SDK against mocked Supabase HTTP — never live credentials or real email addresses. Captures go to an ignored .impeccable/review/; set AUTH_SKIP_SCREENSHOTS=1 to omit them.

The database checks run against real roles

Two SQL scripts exercise the schema with real authenticated and anon roles using simulated JWT subjects, in rollback-only fixtures:

  • tests/auth/profile-rls.sql — ownership, column permissions, nullable removal, timestamps, malformed metadata, cascading deletion, a deliberately failed atomic signup, and the RLS event trigger.
  • tests/auth/profile-migration.sql — passed before cutover, preserving existing and cleared values. Its psql include requires the pre-cutover schema, so do not re-run it against the migrated one.

Anonymous Data API reads of profiles return HTTP 401 / SQL 42501.

Ports

Never use 8080, 8091 or 8092 for an auth preview — other processes own them. Check ownership immediately before starting a server, and never stop a process you do not own.

$probe = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, 8095) try { $probe.Start() } finally { $probe.Stop() } node node_modules/http-server/bin/http-server app -a 127.0.0.1 -p 8095 -c-1

Serve only app/, never the repository root. If the port is taken, inspect the owning process and reuse it only if it is this checkout’s preview; otherwise choose another verified-free port and allowlist its callback URLs.

To try a connected development project:

http://127.0.0.1:8095/index.html?workspace=auth&flow=login

Still true without an account

Everything on this page is about identity and profile. It does not change the rest of the security posture:

  • The market data is still keyless. Every Binance route called is a public read.
  • No broker or exchange is contacted. There is no order to place.
  • Nothing is uploaded from a run. Excluding the account itself, the export path makes no network call at all.
  • The workspace is still a static page. Binding it to loopback remains worth doing on a network you do not control.

Authentication guards a profile. It does not turn a shared machine into a private one: the workspace, its cached bars and its saved runs still live in your browser’s own storage and are reachable by anyone who can open the page. See Storage & persistence.

Last updated on