Skip to content

Security

Five separate systems. One grant model.

This page is written for whoever has to sign HABLA™ off — the technology director, the district counsel, the security questionnaire nobody enjoys. Every claim on it can be checked against a schema, a privilege list or a stated absence, and where a sentence is a commitment rather than a control already running, the sentence says so where it stands. A security page that blurs those is worth nothing to the person reading it.

Request a walkthroughRead the privacy model

Everything here can be checked against something

The data architecture is built, and you can ask to see any of it. The databases are migrated, with their roles, their privileges, their row policies and their retention columns in place — and a verification step reads those privileges out of the running database rather than out of the migration that asked for them, because a migration records an intention and a catalogue records a fact.

Everything here is a property of a schema, a privilege list or a stated absence. That is what makes it checkable: a privilege is in the catalogue or it is not, and a reader can ask to see either. No uptime, performance or availability figure appears on this page — a number like that is worth something in an agreement with a service level attached to it, and worth nothing on a page. What HABLA runs in production is this website, together with the process behind its contact form and the process behind its chat, and those are counted and described further down.

Where a sentence is a commitment rather than a control already running, it says so where it stands — beside the claim it qualifies, rather than gathered into a note at the end, which is a note nobody reads at the moment it would have mattered.

The useful test for a page like this one: can a reader check the claim against something? Everything here is checkable against a schema, a privilege list, or a stated absence.

Five separate systems

These are not schemas inside one database, and not one table with a column telling tenants apart. They are separate databases, and that is what makes the separation something other than a convention: the three data domains the privacy model describes cannot hold a foreign key to one another, because a foreign key does not cross a database.

Five separate systems and three data domains is not an arithmetic error. A domain is a cut of what HABLA may know about a person, and there are three. The last two here are neither: the cross-organization layer is where figures from more than one customer meet, and the sealed store keeps a closed period’s already-suppressed figures so that they outlive the anonymous activity counters they came from. The count was four until the sealed store was built, and it is part of the claim — a page enumerating what a company runs has to be corrected by whatever adds to it, which is the property the log-surface count further down carries and has twice been corrected for.

Anonymous activity
Counters, and nothing else in it. A counter is not a log of interactions: an interaction adds one to the row whose values match it, alongside every other interaction that matched the same values, so no row corresponds to one person’s visit. The time on a row is a calendar date and a whole hour — this store holds no exact timestamp at all. The columns that would describe somebody are not columns it has, so there is nowhere to put one.
Voluntary contact
One table, holding what somebody chose to give when they asked to be contacted and the recipient they chose to reach. It is the most identifying thing HABLA holds, it is the only store that holds identity at all, and it is a separate database from every other one on this list.
Platform configuration
The customer’s own deployment — sites, audience packs, placements, the topic library, resources, named recipients — together with the administrator accounts that manage it. Administrators are staff, not the people the product is for, and this store holds no audience-member identity of any kind. Since the migration that added them it also holds what those accounts sign in with: a password hash, a second-factor secret, single-use recovery codes, a session record and a failed-attempt counter — and that counter is keyed to the account rather than to a network address, which is worth saying because a login throttle is the usual way an address gets kept by a system that promised not to keep one.
Cross-organization aggregates
Its own database, on purpose, because it is the one place figures from more than one customer meet. It receives what a deployment sends it and never queries a customer’s own data — and the breakdown it receives before a figure is published is held to the same rule as the anonymous counters: no reading role reaches it, and the publication gate sees a contributor count, an event total and two concentration shares rather than a row.
Sealed report versions
The newest store, and its own database as well. When a reporting period closes, the figures a report showed are written here once and never changed, so that a report published in one year still resolves to the same figures in the next, and so that a decision recorded against those figures can be checked against what was actually shown. What a row holds is an organization, a period, which breakdown, and counts that have already cleared both floors — a cell that did not clear them is stored with no number in it at all, rather than with a number somebody could reach. Nothing queries a table in it, and the next section says what each of its roles may do instead. Nothing has sealed a period yet, so it is empty today.

What keeps two records apart is not only the missing key. HABLA created no join key that would pair an anonymous row with a contact row, and it never will. But a small enough group of counted events, plus the hour a request arrived, would narrow the pairing for anybody able to read both sides — so the control that actually holds is the one in the next section: nobody holds a read on the raw side at all.

And one residual, recorded rather than argued away. The database engine stamps its own internal version numbers on every row, which HABLA does not declare and cannot drop. Reading them requires a table-level read privilege that no reporting, ingestion or deletion role holds. Those numbers come from one counter that a whole database server shares, and all five of these databases sit on one server today — so the same rising value appears on rows in the anonymous store and on rows in the contact store, and that is the half of this residual that qualifies the paragraph above rather than confirming it. A routine that rewrites those rows so that those numbers stop distinguishing them exists and covers every row, including the current day’s; scheduling it is operational work still ahead of us, it is a control inside the anonymous store and does nothing about the shared counter. And it does not reach a copy. A backup, a replica, or the server’s own write-ahead log carries the original numbers whichever side of that routine it is taken from, together with the moment each write was committed. So who may read those copies is an operational control rather than a database one, and we would rather say that than let the routine read as a fix it is not.

Separate databases on one server are a weaker guarantee than separate servers, and whether production may share one is an open decision rather than a settled one. It is written down as open.

What each role is allowed to do

A view that hides small values is half a control. The other half is the privilege, because a role that can read the table underneath simply reads the table instead. So the interesting question is never which views exist — it is what each credential is allowed to touch.

The path that writes activity holds no read privilege
Recording an interaction means inserting into a write-only view, and the increment itself happens inside a routine owned by the schema owner. The role that ingests activity is granted no privilege at all on the tables behind those views — not read, not insert, not update. It never needs to read what it writes, so it cannot.
Reading a figure means going through the suppression views, and only those
The credential that seals a closed reporting period holds a read on the five views that have already applied the reporting floor of ten activity events, and the job that pushes an organization total to the cross-organization store holds a read on one of them. No other role in this store holds a read of any kind, and no role holds a table-level read on any of the three counter tables underneath. Application-level suppression alone would not survive a support script or an ad hoc query; a revoked read does.
The floor is a stored constant with nowhere to put an exception
The floor lives in the database as a function that takes no arguments — no organization to pass, no credential to look up, and no table for a per-customer threshold column to appear on. That is the enforceable form of a refusal HABLA has already made in writing: a floor sold for money is not a floor, and the shape of the code is what keeps it from becoming one UPDATE statement.
Each floor is enforced in a different place, and this page says which
The floor of ten activity events is in the database itself, and the entries above are how it is held there. The population floor of fifty people is enforced somewhere else, and this is where. A covered population is a band an organization declares about its own map when a placement is set up — a fact about the organization, and not about any person who tapped something — so it is kept in the configuration store, and the anonymous activity counters carry no column that names a population at all. That separation is the boundary no query crosses, which is why the floor is applied where the two are finally brought together: in the program that composes and seals a closed period’s figures, the only thing in this system that reads both. A placement or an audience pack covering fewer than fifty people gets no row of its own there, and its activity is added into the level above it.
On the anonymous counters, the deletion role can see the columns it deletes by, and no others
The role that runs retention there holds a delete on the counter tables plus a read on the three columns its delete condition needs, and on no others — never a count. Stated exactly, because a residual nobody names is a residual nobody reviews: it can tell which combinations of organization, topic and date exist. It cannot tell how large any of them is. That is this store’s shape and the widest of the five; the sealed store’s is the narrowest and is below.
In the sealed store, the roles that exist to touch a figure are the ones that cannot read one
There is no view in this store, and the role that answers a reader holds no privilege on its four tables: it may call one routine, which takes no coordinate, so a single cell cannot be asked for and a whole breakdown arrives or none of it does. The role that writes a sealed version cannot read back a figure it wrote: it seals every organization in one pass, so a credential that could read its own writes could enumerate every customer’s sealed figures in one. The role that runs retention here holds no privilege on any table either, and its sweep takes no arguments at all — no cutoff, no organization — so there is no way to purge a sealed figure early and no way to sweep one customer’s alone.
On the contact store, writing and reading are different roles
The role that files a contact request holds an insert and no read at all. The role that works one holds a read, and may update the three workflow columns the workflow needs and no more. The role that oversees the queue reaches a view and holds no privilege on the table.
And the owner, which is the honest part
PostgreSQL gives a table’s owner privileges that cannot be durably revoked, so “no role can read it” is a statement about the roles a running system connects with. The control on the owner is that its credential belongs in no running service — that is an operational control, and calling it an operational control rather than a grant is the difference between a claim you can check and one you cannot.

One thing that shape does not extend to, said here rather than left to be assumed: in the configuration store, the roles that read a customer’s own configuration read its tables directly. That store holds no audience-member data, and the reading role holds no privilege on the eight tables in it that hold people — administrators, named recipients and content reviewers, and the five that arrived when administrators gained the ability to sign in: their credentials, their second factors, their recovery codes, their sessions, and the failed-attempt counter. It said three until the day the fifth of those was created, and the count is part of the claim: a page that lists what a credential cannot reach has to be corrected whenever something new is added for it to reach.

And the exact thing that does not buy, because a table is not a person. The reading role reaches one view of recorded organizational decisions, and that view resolves whoever owns a decision into a label — which, where the owner is a person, is an administrator’s name. So the accurate sentence is that the reading credential cannot query the administrator table and can still read an administrator’s name through that one view. That is the same disclosure the feature register already makes about the Action Center. Saying instead that the people in that store are beyond the reading credential’s reach would be true about the tables and misleading about the people, and a security page is the worst place on this site to make that trade.

And a second reach, which is a table and not a view — written down because the paragraph above would otherwise read as the whole account. This store keeps references to members of staff on rows that are about something else entirely, and there are four such tables and the reading role reads all of them: who retired a piece of content, who wrote and who published a version of it, which content reviewer cleared it, who declared the population band of a deployment, and — since the migration that added a standing watch on a topic — who opened that watch. Nothing has opened one, and the privilege is what it is either way. Every one of those is an identifier rather than a name. The table that holds names is still the one the reading credential has no privilege on, and the single view named above is still the only place it turns an identifier into a name. What it can tell without one is that the same person did these things and a different person did those. Only the last of the six arrived with that migration. The other five have been true for longer than this paragraph has existed, and what was wrong was this page not saying so rather than the privilege itself — because “no privilege on the eight tables” invites the conclusion that no member of staff appears anywhere in what the reporting layer reads, and that conclusion is not true.

Which organization a session may see

Every table in the configuration store carries a row policy, and so does the contact request table. A session declares which organization it is acting for, and every row of customer data it can read or write is that organization’s. A small, named set of rows in that store belongs to no organization at all, and the next paragraphs say which — because a page that stated the rule and left out its deliberate exceptions would be describing a database HABLA does not have.

Customer data fails closed. Every policy over a customer’s own rows tests the declaration, so a session that has not declared an organization reads none of them — not another organization’s, and not any. It is shown zero rows rather than shown everything, and that is the property worth checking in any system that claims tenant isolation, because the opposite default is the one that produces the incident.

Three kinds of row deliberately belong to no organization, and they are named here rather than left for somebody to find. Content HABLA supplies to every deployment — the topic library, the resources, and the version and reviewer records attached to them — which every customer can read and no customer can write. The store’s own copy of the four thresholds this product is built around, which every credential that reads the store can read and no credential may change. And the benchmark accounts, which are HABLA’s own and no customer’s. Their policies match a session that has declared nothing, on purpose: there is nothing about a customer in those rows for a declaration to narrow, and a categorical “an undeclared session matches no row anywhere” would be false about every one of them.

And the deletion credential has a second mode that is cross-organization by design. With an organization declared it is the deletion cascade for that customer and is confined to it. With nothing declared it is the retention sweep, and it reaches exactly the rows whose own retention anchor has already fallen due — a deactivated administrator, recipient or content reviewer, a decision record past its period, a contact request past its own — across every organization at once, because a sweep that had to be told which customer to sweep is a sweep somebody has to remember to run. A serving administrator’s record is outside that credential’s reach in either mode. Two short-lived tables are not, and they are named here rather than left to be found: an expired session and a lapsed failed-sign-in counter belong to administrators who are still in service, and the sweep removes them. On those two the credential holds no privilege at all — narrower than what it holds on the tables it does sweep — because that removal runs inside a routine owned by the store’s owner rather than through a grant of its own. And a session cannot be in both modes at once.

And here is exactly what it buys. Enforcing the rule in the database makes the declaration mandatory on every path into the store, including the support script and the ad hoc query that application-level scoping never sees. It cannot make the declarationtrue: the session states its own organization, so the real boundary is whatever stands between a signed-in administrator and that statement — and that is the administrative console, which is built now and is reachable by nobody. Isolation is enforced in the database; the authentication that decides who may make that declaration is written, and has never answered a request from outside the machine it was built on.

Row policies do different work in different stores, and in two of the five they are absent. The anonymous counters and the cross-organization ledger grant no reader a table read in the first place, so there is no read for a policy to narrow. A policy there would be decoration on a door that is already welded.

The sealed store does not fit either form, and counting it either way would be wrong. It has row policies on every one of its tables and none of them lets anybody read a figure. They govern writing — which period may be sealed, and only after it has closed — plus a single read of the register that holds no figure, for the one role that has to know whether a version already exists. The absence is the part that was designed: with row-level security switched on and no read policy over the figures, granting a read of them later would return no rows until somebody wrote a policy as well. That is a second line, in a second file, under the paragraph explaining why there is none — which is a deliberate second lock rather than a missing one.

There is also a guard on bulk deletion that tests which identity is executing rather than trusting a flag the caller sets, because a flag any caller can set is a naming convention rather than a guard.

Retention is a column, not a promise

Every table holding data derived from a person is anchored to a column a deletion job can act on — its own, or, where a row’s whole lifetime is another row’s, the parent’s — and to either a documented period or a documented state that ends it. Anchored the second way are three, and they are named rather than left inside the word every: what an administrator signs in with. A retention rule with no column behind it cannot be reviewed, so HABLA treats one as absent rather than as a rule.

  • Anonymous activity: twenty-five months, by date.
  • The figures a closed report showed, sealed so that a report goes on resolving to what it showed: seventy-two months, counted from the first day of the period. That is a different clock from the anonymous activity above, and the one thing that sets its length is this: six years is the longest life this system will give a record that could cite one, so a sealed figure lasts exactly as long as the longest-lived decision able to point at it, and not one month more. What is kept is a total a report already published — one that had cleared the floors before it was written, with everything below them already removed — so the counters it was drawn from can go on their own date without taking it with them. The database computes the figure’s date from the period itself rather than taking it from a writer, so it cannot be lengthened: not by a setting, not by an argument to the deletion job, not by a later statement.
  • The register of which versions were sealed, which holds no figure at all — the period, a fingerprint of what was shown, and the moment the figures went: one hundred and forty-four months, six years longer than the figures themselves. That difference is the only rule on this list whose purpose is to outlive what it describes, and its length is not chosen: it is the seventy-two months the figures themselves last, plus the longest life this system will give a record that could cite one — six years, which is the outer bound the database itself refuses to write past. A decision recorded in an organization’s own console may cite a sealed version for as long as that record is kept, so when the figures have gone HABLA would rather answer that they are no longer held and say when they went than answer nothing at all — an absence cannot tell a reader which of the two happened. On the same clock and holding no figure either: the record of a refused attempt to seal a closed period a second time.
  • A contact request: ninety days by default, written when the request is made, and a contract may set a different period through that same column.
  • An administrator, a named recipient or a content reviewer: ninety days from the day the account is deactivated, not from the day it was created.
  • What an administrator signs in with — the password verifier, the second-factor secret and the single-use recovery codes: destroyed at the instant the account is deactivated, not ninety days later. The window HABLA gives the record of who somebody was is not a window for the thing that signs them in. These are the three anchored on a parent: the column is the account’s, one join away, deleting the account deletes them, and a column of their own could be set to outlast it.
  • A signed-in administrator’s session: not retained at all. It ends on whichever of its clocks runs out first, and there are three of them: twelve hours from the moment it was created, at the outside; thirty minutes of inactivity; or a revocation by a sign-out, a password change, a second-factor reset, a change to which roles the account holds, or a deactivation. An ended session is refused the moment it is presented, whether or not its row has gone yet, and removing the row is the same unscheduled program as everything else on this list. This store has nowhere to keep a history of sign-ins, and that is deliberate.
  • The counter of failed sign-in attempts: fifteen minutes, and the row goes once both the counting window and any lock on the account have lapsed. A successful sign-in deletes it outright rather than setting it to zero, because a row at zero still says somebody had trouble getting in. There is one row per account holding a count and the instant the window opened — no row per attempt, no time of day, no address.
  • A record of an organizational decision: the contracted history, defaulting to three years where no contract sets one — and never further out than six years from the day the decision was taken, because the database refuses a longer date rather than trusting the program that writes it. That outer bound is what the register above is measured against, so no record can outlive the evidence it cites.
  • The query log behind integrity checks: one hundred and twenty days, written as the budget window plus the look-back rather than as a round number.
  • The record that a deletion happened: no expiry, deliberately — every field in it is about the deletion rather than about anybody, and it is the only evidence a deletion was carried out.
  • The pre-publication ledger in the cross-organization store, which holds how much each deployment contributed to a figure: no period and no timer, deliberately — the rows are deleted when the figure they belong to is published or rejected, which is a state and not a duration. This is the one rule on the list that is a state transition, and giving it a grace period would be inventing a retention window nothing grants.

What is not done, beside the claim it qualifies. The deletion jobs are written — five of them, one per store — and nothing schedules any of them. Each connects as a narrow database account that carries no password anywhere in this repository’s source — deliberately, because a credential in a source file is a credential in the repository — and until those accounts existed that was a second reason none of them could start. They were provisioned out of band, four of them together and the fifth with the store that needed it, so a missing credential is no longer what stops them; what stops them is that nothing schedules them, and this page says so rather than letting the older reason stand. What a period does enforce today is a read boundary, and the honest form of that says where: on a contact request, past its instant the row policies hide the row from every application role, so it is unreadable and still there. An administrator’s session reaches the same effect by a different mechanism — the routine that looks a session up refuses one past its expiry or past the idle window — and the failed-sign-in counter has no such boundary and needs none, because a window that has lapsed counts as nothing to the only thing that reads it. A read boundary is a real control and it is not deletion, and saying which is which is the point of this paragraph. Automating the deletion is operational work that has not been built, and until it is, the accurate sentence is that the rules exist and are enforced as a read boundary rather than that deletion is automatic.

Administrator identity, and the line it does not cross

An administrator is staff, with a name and a work address, and those live in the configuration store beside sites and placements. They are not in the same database as anything the person’s side of the product produces, and there is no route from one to the other.

There is no column that points across. Nothing in the configuration store references the anonymous counters; the counters carry the deployment’s own configuration values and nothing about whoever produced them; and the stores are separate databases, so a key between them is not something a future migration could add by accident. There is no cross-database link configured in either direction, and a check asserts that none appears.

Administrator identity is not permitted in the person’s experience — and that is still a commitment rather than something you can read out of a catalogue. The experience it would appear in is built now, which changes the sentence rather than strengthening it: it is no longer true merely because there is no code. The honest form is the one the threat model states — no such path is permitted, and any path that appeared would be a Critical finding rather than a design question — with the difference that the code it is a statement about now exists and can be read. What is checkable in a database catalogue today is the paragraph above it.

Administrators are also people, and are treated as such. Their records carry a deactivation date and the same ninety-day deletion rule as any other person-derived record, which is the part of an administration model that usually has no rule at all.

The authentication itself is built and is reachable by nobody, and it is listed below with everything else in that state rather than described as though it were in service.

What the logs hold

Telemetry exists so that infrastructure can be kept healthy, and a log that lets somebody reconstruct one person’s journey is a defect whatever it was collected for. The rule itself, and the pairings a log must never hold together, is on the privacy page. What belongs here is the evidence, split into the half that has been checked and the half that has not.

Checked

The log-writing surfaces HABLA operates on this host are four today. Three of them sit in the path of a request a visitor makes: the origin server that serves this website, the process behind the enquiry form on the contact page, and the process behind HABLA AI, the chat. The fourth is the PostgreSQL database cluster, and it is here because the test this paragraph applies to the administrative console — is it installed, is a port listening — admits it. Scheduled work on this host writes a line each time it runs as well, and is named here rather than left to be found: a nightly bundle of the repository, and a weekly refresh of the list of edge addresses the origin will answer. None of them writes a visitor’s address, and each of the four is checked below on its own terms rather than covered by one sentence about the others.

This count is part of the claim, and it has now been wrong twice. It said twountil the chat went live, and three until the database was counted — a page that enumerates what a company operates has to be corrected by whatever adds to it. Both corrections came from somebody reading the page against the machine, and that is the weak point rather than the reassuring part: the number is maintained by attention rather than by a test. A guard that goes red the day this host gains a process is written down as an open gap. The administrative console still does not make it five, and that reason is checkable rather than reassuring: it is not installed on this machine and no port is listening for it. The day it is put into service, this number is one of the things that has to change with it.

The origin server. Request logging is off at the server level rather than in one block, so a block added later does not inherit a default that writes them. Both address fields are removed from the log format itself, so a log switched back on could not reintroduce them by accident. Error logs are discarded rather than kept, because the error format stamps an address onto every request-level entry. And the module that would resolve a visitor’s address from behind the network edge is deliberately left unconfigured, so this server never resolves it at all. The configuration block that serves the enquiry form declares no logging of its own, so it inherits every one of those rules rather than starting from a default.

The enquiry endpoint. A visitor’s address is used here and in the chat, and nowhere else in HABLA. This endpoint writes two kinds of line: the address it is listening on when it starts, and — when a message fails to reach the mailbox — which step of the mail conversation failed and the code the mail server answered with. Neither kind carries a field anybody typed, and neither carries the sender’s address. The address becomes a rate-limit key instead: a keyed digest of it, computed under a random value the process generates when it starts and forgets when it stops, held in memory beside a count for the length of the window and written to no file and no database. The process holds no database connection of any kind, so there is nowhere for it to write one. And the code that holds the count is only ever handed a key and a clock, so it could not put that key beside a name, an address or a message even if somebody asked it to.

HABLA AI. The chat is a second process of the same shape, and it was read the same way rather than covered by the paragraph above. It writes two kinds of line: the address it is listening on when it starts, together with the names of the documentation files it loaded, and — on a failure — which stage failed and, for a refusal by the model provider, the status code that came back. Neither kind carries a conversation, a turn, a field anybody typed or the visitor’s address. The visitor’s address becomes a rate-limit key by the same code the enquiry endpoint uses: the same keyed digest under a random value new on every start, the same count in memory for the length of the window, written to no file and no database. The process holds no database connection either. What is different here, and is stated rather than left to be discovered, is that the conversation itself leaves HABLA’s infrastructure: it is sent to Anthropic to write each reply. What that means, in full, on the privacy page.

The database cluster. PostgreSQL runs here because the databases are built and tested on this machine. It listens on this machine’s loopback interface and nowhere else, so a connection to it can be opened only from inside this machine. Neither process a visitor can reach carries a database credential or a database driver, and no line of code in either of them names one, so neither can open that connection on a visitor’s behalf. It is not in the path of any request this website serves. What it writes is what a database writes when something goes wrong: the text of the statement that failed and the detail underneath it. Statements that succeed are not written, connections and disconnections are not written, and every line carries a timestamp and a process number and no client address. Every path that reaches it starts inside this machine, in development or in test work — so what is in that file did not come from anybody using HABLA, and that is the claim to check the day this page is read against a running product.

That is the point of the design on the three surfaces a request reaches: the promise holds by construction — a module left unconfigured, a random value that is new on every start — rather than by a retention setting somebody has to remember. The fourth is different and is worth saying plainly: a database writes what a database writes, and what keeps a visitor out of it is that no path leads from a request to it.

Not checked

The services HABLA configures but does not itself write — the network edge, a content network, an error tracker — and every surface that is not in service yet, the administrative console among them. A full logging audit is scheduled and has not run.That second half is stated as not in service rather than as does not exist, deliberately. It used to say the latter, and that wording is what let a HABLA-operated process go live while both registers still described the world before it: a thing that exists but is not running belongs on this side of the line too, and a band that only admits things which do not exist has nowhere to put it.

Its result is binding in both directions. If it finds request-level retention at a service HABLA cannot switch off, the published claim changes rather than being reworded to fit the finding. That is written down as the consequence in advance, which is the only moment such a rule can be written honestly.

Ask for the part that sounds too strong.

A privilege list, a row policy and a retention column are things somebody can be walked through. If a sentence on this page cannot be shown to you in that form, it should not be on this page — and that is the conversation worth having before a contract, not after one.

Request a walkthroughHow HABLA counts