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' // defaultIt 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:
| Route | Purpose |
|---|---|
index.html?workspace=auth&flow=login | Sign in |
index.html?workspace=auth&flow=signup | Create an account |
index.html?workspace=auth&flow=confirmation | Email confirmation callback |
index.html?workspace=auth&flow=reset | Request a password reset |
index.html?workspace=auth&flow=recovery | Set a new password |
index.html?workspace=account§ion=profile | My Account → Profile |
index.html?workspace=account§ion=security | My 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
| Flow | Supported | Notes |
|---|---|---|
| Sign in / sign up | Yes | Email and password |
| Email confirmation | Yes | Resend and status display are implemented; whether confirmation is enforced is a dashboard setting, not code |
| Password reset → recovery | Yes | Recovery takes precedence until completed or cancelled |
| Change email | Yes | Pending and current addresses are distinguished |
| Sign out | Yes | Local scope; never clears optimizer or theme storage |
| OAuth providers | No | Email/password only — no OAuth, OTP or SSO anywhere in the client |
| MFA / 2FA | No | Not present in the client |
| “Remember me” | No | Session persistence is always on, with no user control |
| Delete account | No | Not offered anywhere |
| CAPTCHA | No | This 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.
SELECTandUPDATEare limited to the row where(select auth.uid()) = id.- Ownership is required in both
UPDATEconditions. - Authenticated browser grants are
SELECTplus column-levelUPDATEof name and phone only. - Browser
INSERT,DELETE, owner changes and timestamp writes are all denied. - Trigger functions live in a non-exposed
account_privateschema 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:
npm cinpm run auth:configureThat 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
- Authentication → URL Configuration — keep existing redirects and add the three
account.html?flow=callback URLs (confirmation, recovery, email-change). - Keep the Confirm signup, Reset password and Change email address templates
using
{{ .ConfirmationURL }}. The client uses implicit flow; a custom server callback ortoken_hashtemplate is not implemented. - Email/password enabled, confirm email enabled, minimum password length at least 8.
- 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.
- 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 defaultBrowser 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. Itspsqlinclude 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-1Serve 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=loginStill 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.