Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Refer to the [**Quick Start Guide**](docs/QUICKSTART.md) for detailed onboarding

- ✅ **Open source** (Apache 2.0)
- ✅ **Privacy-by-design** (Immutable consent ledger, orchestrated erasure)
- ✅ **Cloud-native** (Kubernetes-ready, Kafka event-driven, stateless)
- ✅ **Cloud-native** (Kubernetes-ready, OpenTelemetry-native, stateless)
- ✅ **Infrastructure Agnostic** (Support for PostgreSQL/MySQL, Kafka/RabbitMQ)
- ✅ **High Performance** (gRPC aggregation at the gateway layer)
- ✅ **Compliant** with GDPR, ePrivacy, and emerging data laws

Expand Down Expand Up @@ -71,17 +72,18 @@ PCM uses **Hexagonal Architecture** with clear **bounded contexts**, standardize

| Service | Responsibility | Stack |
|---------|----------------|-------|
| **Profile Service** | User identity, handle management, dynamic attributes | PostgreSQL (JSONB), Redis |
| **Consent Service** | GDPR consent collection, versioning, legal proof | PostgreSQL (Ledger) |
| **Segment Service** | User classification and real-time segmentation | Elasticsearch, Kafka |
| **Profile Service** | User identity, handle management, dynamic attributes | PostgreSQL/MySQL (JSON), Redis |
| **Consent Service** | GDPR consent collection, versioning, legal proof | PostgreSQL/MySQL (Ledger) |
| **Segment Service** | User classification and real-time segmentation | Elasticsearch, Kafka/RabbitMQ |
| **Preference Service** | UX preferences (language, theme, notifications) | Redis |
| **Config Service** | Centralized configuration for the entire platform | Spring Cloud Config |
| **API Gateway** | Unified entry point, JWT security, Aggregator | Spring Cloud Gateway, gRPC |
| **Config Service** | (Optional) Centralized configuration | Spring Cloud Config |
| **API Gateway** | Unified entry point, JWT security, Aggregator | Gateway, gRPC, OTel |

### Communication

- **Synchronous**: gRPC for high-performance data aggregation (Gateway -> Services)
- **Asynchronous**: Kafka with Avro schemas for cross-service events (e.g., GDPR Erasure)
- **Asynchronous**: Spring Cloud Stream with support for **Kafka** (Avro) and **RabbitMQ**
- **Observability**: **OpenTelemetry** native (TRACES & METRICS exporters)
- **Security**: JWT for client authentication, standard OAuth2 resources server

---
Expand Down Expand Up @@ -109,7 +111,11 @@ pcm/
---

## ⚙️ Configuration
PCM uses a centralized configuration model via **Spring Cloud Config**. All core platform settings (Kafka, Redis, Vault, Logging) are managed in the `config-service`.
PCM uses a dual configuration model:
1. **Centralized**: Via **Spring Cloud Config** (recommended for production).
2. **Decentralized**: All services provide local fallbacks and can be configured entirely via **Environment Variables** (ideal for local dev or simple container deployments).

Core platform settings (Messaging, Database, Vault, Logging) are standardized in the `config-service`.

- **Source of truth**: `config-service/src/main/resources/config/`
- **Shared config**: `application.yml`
Expand All @@ -119,7 +125,8 @@ PCM uses a centralized configuration model via **Spring Cloud Config**. All core
## 📚 Documentation

- [**Quick Start Guide**](docs/QUICKSTART.md) - Get PCM running locally in 5 minutes.
- [**API Reference**](docs/API_REFERENCE.md) - Endpoints, payloads, and Examples.
- [**API Reference**](docs/API_REFERENCE.md) - Endpoints, payloads, and examples.
- [**Dependency Monitoring**](docs/DEPENDENCY_MONITORING.md) - Guide to monitoring updates and vulnerabilities.
- [**Architecture Decision Records**](docs/architecture/) - Design decisions and rationale.
---

Expand Down
12 changes: 10 additions & 2 deletions api-gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

<!-- Rate Limiting (Redis) -->
<dependency>
Expand All @@ -62,7 +70,7 @@
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
<version>${grpc-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
Expand All @@ -79,7 +87,7 @@
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
<version>${logstash-logback-encoder.version}</version>
</dependency>

<!-- Redis -->
Expand Down
15 changes: 14 additions & 1 deletion api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@ spring:
application:
name: api-gateway
config:
import: "optional:configserver:http://localhost:8888"
import: "optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888}"
cloud:
config:
fail-fast: false
gateway:
routes:
- id: profile-service
uri: ${PROFILE_SERVICE_URL:http://localhost:18081}
predicates:
- Path=/api/v1/profiles/**
- id: consent-service
uri: ${CONSENT_SERVICE_URL:http://localhost:18083}
predicates:
- Path=/api/v1/consents/**
2 changes: 1 addition & 1 deletion config-service/src/main/resources/config/api-gateway.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: ${JWT_ISSUER_URI:http://localhost:8090/auth/realms/pcm}
issuer-uri: ${JWT_ISSUER_URI:http://localhost:8090/realms/pcm} # Keycloak URL without /auth

resilience4j:
circuitbreaker:
Expand Down
25 changes: 24 additions & 1 deletion config-service/src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ spring:
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
dialect: ${DB_DIALECT:org.hibernate.dialect.PostgreSQLDialect}
format_sql: true
jdbc:
time_zone: UTC
Expand Down Expand Up @@ -43,6 +43,12 @@ spring:
configuration:
schema.registry.url: ${SCHEMA_REGISTRY_URL:http://localhost:8081}
specific.avro.reader: true
rabbit:
binder:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:guest}
password: ${RABBITMQ_PASSWORD:guest}
bindings:
# Producers (profile-service)
profileCreated-out-0:
Expand Down Expand Up @@ -87,6 +93,13 @@ spring:
enabled: true
backend: secret

# Security Abstraction
security:
pii:
provider: ${PII_PROTECTION_PROVIDER:vault} # Options: vault, local
local:
secret: ${PII_LOCAL_SECRET:abcdefghijklmnop} # 16 chars for AES-128

# Shared Management & Observability
management:
endpoints:
Expand All @@ -105,6 +118,16 @@ management:
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318/v1/traces}
otlp:
metrics:
export:
url: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics}
enabled: true
tracing:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318/v1/traces}

# Shared Logging
logging:
Expand Down
12 changes: 10 additions & 2 deletions consent-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
<version>${grpc-spring-boot-starter.version}</version>
</dependency>

<!-- Spring Cloud Stream Kafka Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

<!-- Spring Cloud Config -->
<dependency>
Expand Down Expand Up @@ -97,6 +101,10 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

<!-- OpenAPI Documentation -->
<dependency>
Expand All @@ -116,7 +124,7 @@
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
<version>${logstash-logback-encoder.version}</version>
</dependency>

<!-- Testing -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

Expand All @@ -18,8 +20,8 @@
*/
@Entity
@Table(name = "consent_ledger", indexes = {
@Index(name = "idx_consent_profile", columnList = "profile_id, timestamp DESC"),
@Index(name = "idx_consent_purpose", columnList = "profile_id, purpose, timestamp DESC")
@Index(name = "idx_consent_profile", columnList = "profile_id, timestamp DESC"),
@Index(name = "idx_consent_purpose", columnList = "profile_id, purpose, timestamp DESC")
})
@EntityListeners(AuditingEntityListener.class)
@Getter
Expand Down Expand Up @@ -61,12 +63,13 @@ public class Consent implements AggregateRoot<UUID> {
@Column(name = "proof_hash", nullable = false, length = 64)
private String proofHash;

@Column(columnDefinition = "JSONB")
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "metadata")
private String metadata;

private Consent(String tenantId, UUID profileId, ConsentPurpose purpose, boolean granted,
String version, String consentText, String ipAddress,
String userAgent, String proofHash) {
private Consent(String tenantId, UUID profileId, ConsentPurpose purpose, boolean granted,
String version, String consentText, String ipAddress,
String userAgent, String proofHash) {
this.id = UUID.randomUUID();
this.tenantId = tenantId;
this.profileId = profileId;
Expand All @@ -79,15 +82,15 @@ private Consent(String tenantId, UUID profileId, ConsentPurpose purpose, boolean
this.proofHash = proofHash;
}

public static Consent grant(String tenantId, UUID profileId, ConsentPurpose purpose, String version,
String consentText, String ipAddress, String userAgent,
String proofHash) {
public static Consent grant(String tenantId, UUID profileId, ConsentPurpose purpose, String version,
String consentText, String ipAddress, String userAgent,
String proofHash) {
return new Consent(tenantId, profileId, purpose, true, version, consentText, ipAddress, userAgent, proofHash);
}

public static Consent revoke(String tenantId, UUID profileId, ConsentPurpose purpose, String version,
String consentText, String ipAddress, String userAgent,
String proofHash) {
public static Consent revoke(String tenantId, UUID profileId, ConsentPurpose purpose, String version,
String consentText, String ipAddress, String userAgent,
String proofHash) {
return new Consent(tenantId, profileId, purpose, false, version, consentText, ipAddress, userAgent, proofHash);
}
}
17 changes: 16 additions & 1 deletion consent-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,19 @@ spring:
application:
name: consent-service
config:
import: "optional:configserver:http://localhost:8888"
import: "optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888}"
cloud:
config:
fail-fast: false
# Local fallback for Kafka if config-server is down
stream:
kafka:
binder:
brokers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}

# Local fallback for Datasource
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:8843}/consent_db
username: ${DB_USERNAME:pcm}
password: ${DB_PASSWORD:pcm_dev_password}
driver-class-name: org.postgresql.Driver
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ CREATE TABLE consent_ledger (
granted BOOLEAN NOT NULL,
version VARCHAR(20) NOT NULL,
consent_text TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent TEXT,
proof_hash VARCHAR(64) NOT NULL,
metadata JSONB
metadata JSON
);

CREATE INDEX idx_consent_profile ON consent_ledger(profile_id, timestamp DESC);
Expand Down
49 changes: 39 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,27 @@ services:
image: jaegertracing/all-in-one:latest
container_name: pcm-jaeger
ports:
- "5775:5775/udp"
- "6831:6831/udp"
- "6832:6832/udp"
- "5778:5778"
- "16686:16686"
- "14268:14268"
- "14250:14250"
- "9411:9411"
environment:
COLLECTOR_ZIPKIN_HOST_PORT: :9411
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC (internal collector)
- "4318:4318" # OTLP HTTP (internal collector)
- "9411:9411" # Zipkin (legacy)
networks:
- pcm-network

# OpenTelemetry Collector
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
container_name: pcm-otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./docker/otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8888:8888" # Metrics
depends_on:
- jaeger
- prometheus
networks:
- pcm-network

Expand Down Expand Up @@ -291,6 +302,24 @@ services:
networks:
- pcm-network

# RabbitMQ - Alternative messaging broker for portability verification
rabbitmq:
image: rabbitmq:3-management-alpine
container_name: pcm-rabbitmq
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
interval: 10s
timeout: 5s
retries: 5
networks:
- pcm-network

volumes:
postgresql_data:
redis_data:
Expand Down
29 changes: 29 additions & 0 deletions docker/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
receivers:
otlp:
protocols:
grpc:
http:

processors:
batch:

exporters:
logging:
loglevel: debug
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: "0.0.0.0:8888"

service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging, otlp/jaeger]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheus]
Loading