Build-time PII protection for static sites

Toggle staff visibility across five privacy levels without destroying content. Based on the Superbloom/Draftlab Responsive Transparency research, an evidence-based taxonomy for managing organizational PII exposure.

The Problem

Civil society organizations need public transparency for credibility, but that visibility exposes staff to harassment, doxxing, and physical threats. Current solutions involve destructive deletion that takes hours. Staff need protection within minutes.

The Solution

Responsive Privacy lets you build the same site at different privacy levels. Content is never deleted — it's filtered at build time based on the Attribution Taxonomy's five-tier system:

LevelNameWhat's Visible
0Complete AnonymityNothing — all PII hidden
1Role-Only VisibilityJob titles and departments only
2Professional IdentityNames, roles, project attribution
3Public ProfessionalFull professional profile, no contact info
4Full TransparencyEverything including contact details

Quick Start

1. Install

Terminal window
pnpm add @responsive-privacy/core @responsive-privacy/astro

2. Configure

Create responsive-privacy.config.ts in your project root:

import { defineConfig } from "@responsive-privacy/core";
export default defineConfig({
collections: {
team: {
fields: {
name: "ID-01", // Full Name → visible at Level 2+
photo: "ID-02", // Photo → visible at Level 2+
role: "ID-03", // Job Title → visible at Level 1+
bio: "ID-04", // Biography → visible at Level 3+
email: "CV-01", // Email → visible at Level 4 only
department: "OR-01", // Department → visible at Level 1+
},
},
},
});

3. Add the Astro integration

astro.config.mjs
import { responsivePrivacy } from "@responsive-privacy/astro";
import privacyConfig from "./responsive-privacy.config";
export default defineConfig({
integrations: [responsivePrivacy(privacyConfig)],
});

4. Filter content in your templates

---
import { getCollection } from 'astro:content';
import { filterCollection } from '@responsive-privacy/astro/helpers';
import privacyConfig from '../responsive-privacy.config';
const rawTeam = await getCollection('team');
const team = filterCollection('team', rawTeam, privacyConfig);
---
{team.map((member) => (
<div>
<h3>{member.data.name}</h3>
{member.data.role && <p>{member.data.role}</p>}
{member.data.email && <a href={`mailto:${member.data.email}`}>Email</a>}
</div>
))}

5. Build at different levels

Terminal window
# Normal build — full transparency
astro build
# Threat response — hide identities
PRIVACY_LEVEL=1 astro build
# Emergency — complete anonymity
PRIVACY_LEVEL=0 astro build

How it works under the hood

Each content field is mapped to an Attribute ID from the taxonomy (e.g. ID-01 = Full Name, CV-01 = Email). Each attribute has a privacy level threshold — the minimum level at which it's visible.

At build time, the package reads PRIVACY_LEVEL from the environment, compares it to each attribute's threshold, and either passes the field through, replaces it with a redacted value (e.g. "Staff Member"), or omits it entirely.

Content is never modified or deleted. The same source produces different outputs at different levels.

Packages

PackageDescription
@responsive-privacy/coreFramework-agnostic transformer engine and taxonomy defaults
@responsive-privacy/astroAstro integration, virtual module, and template helpers

Attribution Taxonomy reference

The default attribute definitions ship with the package. Here's the full mapping:

Identity Attributes

IDNameRiskThresholdRedaction
ID-01Full NameHighLevel 2Replace → "Staff Member"
ID-02Photo/HeadshotHighLevel 2Omit
ID-03Job Title/RoleMediumLevel 1Omit
ID-04BiographyMediumLevel 3Omit
ID-05CredentialsLowLevel 3Omit

Contact Vectors

IDNameRiskThresholdRedaction
CV-01Email AddressVery HighLevel 4Replace → "Contact the organization"
CV-02Phone NumberVery HighLevel 4Omit
CV-03Office LocationVery HighLevel 4Omit
CV-04Social MediaMediumLevel 3Omit
CV-05Messaging HandlesHighLevel 4Omit

Organizational Relationships

IDNameRiskThresholdRedaction
OR-01Department/TeamLowLevel 1Omit
OR-02Board MembershipMediumLevel 3Omit (⚠️ compliance protected)
OR-03Partner OrgsMediumLevel 3Omit
OR-04Project AssociationsLowLevel 2Omit
OR-05Advisory StatusLowLevel 3Omit

Temporal/Activity Data

IDNameRiskThresholdRedaction
AD-01Work ScheduleHighLevel 4Omit
AD-02Event ParticipationMediumLevel 3Omit
AD-03Publication DatesLowLevel 2Omit
AD-04Project TimelinesMediumLevel 3Omit
AD-05Bylines/AuthorshipMediumLevel 2Replace → "Organization Staff"

Customization

Override any default threshold or redaction strategy:

import { defineConfig } from "@responsive-privacy/core";
export default defineConfig({
// Override defaults for your organization
attributes: {
"ID-01": {
name: "Full Name",
category: "identity",
risk: "high",
threshold: 3, // Your org wants names hidden more aggressively
redaction: "replace",
redactedValue: "Anonymous",
},
},
collections: {
/* ... */
},
});

Deployment integration

Trigger privacy-level builds via webhook from your CMS or deploy platform:

Terminal window
# Coolify / GitHub Actions / Netlify build command
PRIVACY_LEVEL=${{ inputs.privacy_level }} astro build

For PagesCMS, configure a webhook that passes the privacy level as an environment variable to your build pipeline.

Get the package

Open source under the MIT license. Browse the code, file issues, or pull the package from npm.