Compliance — the notice you publish
Why the grievance officer your privacy page names is Connect's and not yours, and how to read it instead of copying it.
Your application processes personal data. Under India's Digital Personal Data Protection Act, 2023, somebody has to be named as the Data Fiduciary — the party that decides why and how that data is processed — and that party has to publish a grievance officer a Data Principal can complain to (s.13).
On Connect, that party is the platform operator. Your application is a Data Processor acting on their notice. So the channel your privacy page prints, and the one your consent capture points a user at, is theirs.
This is a fact about the platform, not about a workspace
getCompanyProfile() takes no workspaceId and no session. The answer is
identical for every tenant and every caller, because it describes who
operates the thing your application runs on.
Read it, don't copy it
The obvious way to ship a grievance address is configuration:
# Don't. This is the shape the endpoint exists to replace.
APP_DATA_FIDUCIARY_NAME="..."
APP_GRIEVANCE_OFFICER_NAME="..."
APP_GRIEVANCE_OFFICER_EMAIL="..."It works, and then an officer resigns.
Every application that copied the old address is now publishing a redressal channel that reaches nobody — silently, with no error, no failing test and no alert, for as long as it takes a human to notice. A statutory contact is precisely the piece of configuration that must not be able to drift on its own, so it is read at runtime instead:
import { connect } from "@/lib/connect";
const profile = await connect.platform.getCompanyProfile();
profile.legalName; // the Data Fiduciary to name
profile.brandName; // for prose the registered name reads oddly in
profile.registeredAddress;
profile.legalJurisdiction; // the venue a governing-law clause names
profile.grievance.officerName;
profile.grievance.officerEmail; // where a complaint goes
profile.grievance.hours;
profile.grievance.responseDays; // the window the notice commits toThe identity and the whole grievance block are non-nullable. There is nothing here for you to write a fallback for — and that is the point, because a fallback is the stale copy again wearing a different hat.
website, gstNumber and legalJurisdiction are the three that can be
null, and they are optional facts, not unconfigured ones — omit the line
rather than substituting something. legalJurisdiction in particular is not
the registered address: an entity registered in one state routinely names
another as the venue for disputes, and reaching for registeredAddress
instead is a mistake two applications here made, differently.
A 503 is the operator's problem, not yours
There are no server-side defaults behind this endpoint. If the platform
operator has not filled in their company details it answers 503 unavailable rather than inventing an answer — because inventing one is the
entire failure this endpoint exists to remove, and doing it one layer down
would be the same mistake with better manners. Render your page without the
block. Do not add a fallback of your own "just for outages": that is a copy,
and it goes stale exactly like the last one did.
Rendering it
The wording ships with the SDK, so a user comparing two applications' privacy pages does not find two different accounts of who their Data Fiduciary is:
import { ConnectGrievanceNotice } from "@cosmoops/connect-sdk/client";
<ConnectGrievanceNotice applicationName="CosmoOps CRM" />It renders null while loading and null on failure rather than a skeleton or
an error box. A red error box in the middle of a legal notice reads as the
notice itself being broken, and the address is on the public web either way.
Call useConnectCompanyProfile() directly if you want to handle those states.
The hook does not require a session, unlike every other hook in /client —
it is the one caller of useConnectResource's { requiresSession: false }.
That is deliberate: the moment a grievance channel most needs to be legible is
on a public page a signed-out visitor is reading. So keep the route out of
ConnectAccessGate and mark it gateExempt in your nav; a redressal channel
behind a login wall is the shape a regulator objects to.
The component is live on Screens, signed out, rendering this endpoint.
What is still yours
Reading the platform's notice does not make the SDK your compliance layer. These stay your application's problem, because they are about data you decided to collect:
| Yours | Why |
|---|---|
| Consent capture | You chose the purposes. Record what was agreed, when, on which notice version, and over which channels. |
| The consent ledger | The Act puts the burden of demonstrating consent on the Fiduciary. Current state ("opted out") does not say when or on what notice, so log events and never mutate them. |
| Withdrawal | Must be as easy as granting. If consent took one click, so does taking it back. |
| Access, correction, erasure | A Data Principal can ask. Your store, your export, your deletion. |
| Retention | Erase once the purpose is served. That is a judgement, not a timer — flag for review rather than running an unattended purge over a customer's contact book. |
A worked example of all five is an append-only consent ledger: a collection
declared appendOnly in your storeSchema, with no update and no delete path
in your own code either. A ledger a bug can rewrite is not evidence — see
the store for what appendOnly refuses, and to whom.
Caching
The profile changes about never, and the response carries
s-maxage=3600, stale-while-revalidate=86400. Cache it in your own layer too
if you render it on a hot path.
Do not cache it for a release cycle. An hour is the deliberate ceiling on how long a corrected grievance contact can keep being published wrong, which is the failure this whole arrangement exists to bound.
Prerequisite
The endpoint needs the company:read scope on your API key. Existing keys
do not carry it — tick it in the Console's Integration tab, or re-run your
application's seed script, which widens scopes in place.