A specialized microservice for managing user feedback, project reviews, and threaded comments within the Interfacer Project ecosystem. Built with Go, Gin framework, and SQLite, this service handles project ratings (1β5 stars), aggregated review summaries, hierarchical comment threads with attachments, and event-driven rating updates β all secured with DID-based EdDSA authentication.
β οΈ Early Development Stage: This microservice is currently in early stage development and is being actively developed as part of the broader Interfacer Project infrastructure.
- Overview
- Features
- Architecture
- Prerequisites
- Installation
- Configuration
- Authentication
- API Documentation
- Data Model
- Usage Examples
- Events
- Development
- Testing
- Contributing
- License
The Interfacer Feedback & Interaction microservice is a core component of the Interfacer Project ecosystem, designed to handle all user-generated feedback and interaction features. This microservice operates within a distributed architecture where:
- interfacer-gui (frontend) consumes review scores, comment threads, and summary data
- interfacer-proxy acts as the API gateway and routing layer, forwarding signed requests with DID authentication headers
- interfacer-feedback-service (this service) manages reviews, ratings, and comments
- interfacer-dpp manages Digital Product Passport data independently
- Project Service consumes
RatingUpdatedevents to cache aggregated review scores
The service supports community-driven quality assessment and conversation by providing:
- Star Ratings: 1β5 star reviews tied to projects, with one review per user per project enforced at the database level
- Review Summaries: Aggregated average ratings, total counts, and per-rating distributions for display on project cards
- Threaded Comments: Hierarchical comment system with nested replies and optional file attachments
- Event Publishing: Asynchronous
RatingUpdatedevents so the main Project Service can update its cached aggregates in real time
βββββββββββββββββββββ
β interfacer-gui β β Frontend (browser)
ββββββββββ¬βββββββββββ
β
ββββββΌββββββ
β Proxy β β DID auth + routing (x-user-id header)
ββββββ¬ββββββ
β
ββββββΌβββββββββββββββ
β Feedback Service β β Reviews, comments, events
ββββββ¬βββββββββββββββ
β RatingUpdated events
ββββββΌβββββββββββββββ
β Project Service β β Caches aggregated ratings
βββββββββββββββββββββ
- Star Ratings & Reviews: One review per user per project with 1β5 star ratings, optional review text, and on-the-fly updates via upsert
- Review Summaries: Aggregated statistics including average rating, total review count, and per-rating distribution histogram
- Threaded Comments: Hierarchical comment threads with arbitrary nesting depth, soft deletion that preserves thread structure, and optional file attachment references
- DID-Based Authentication: EdDSA signature verification via Zenroom cryptographic VM with DID resolution β identical flow to interfacer-dpp
- Cursor-Based Pagination: All list endpoints support limit + cursor pagination for efficient infinite-scroll UIs
- Event Publishing: Asynchronous
RatingUpdatedevents for inter-service communication (extensible to RabbitMQ, NATS, Kafka) - SQLite Storage: Zero-dependency embedded database with WAL mode for concurrent read performance
- ULID Identifiers: Universally unique lexicographically sortable IDs for all primary keys and external references
- RESTful API: Clean JSON endpoints matching interfacer-dpp conventions for error responses and CORS configuration
- Public Reads, Auth Writes: All
GETendpoints are public;POST,PUT, andDELETErequire DID-authenticated requests
interfacer-feedback-service/
βββ cmd/
β βββ main/
β βββ main.go # Application entry point, route registration, CORS setup
βββ internal/
β βββ auth/
β β βββ auth.go # DID verification (Zenroom + DID resolution)
β β βββ middleware.go # Gin auth middleware (skips GET/OPTIONS)
β β βββ zenflows-crypto/ # Embedded Zenroom verify script (git submodule)
β βββ database/
β β βββ database.go # SQLite connection (sync.Once singleton), migrations
β β βββ review_repo.go # Review CRUD + summary aggregation queries
β β βββ comment_repo.go # Comment CRUD + soft delete queries
β βββ events/
β β βββ events.go # Event publisher interface + RatingUpdatedEvent schema
β βββ handler/
β β βββ reviews.go # POST/GET/DELETE reviews, GET review summary, GET user's review
β β βββ comments.go # POST/GET/DELETE comments
β βββ model/
β βββ model.go # Data structures: Review, ReviewSummary, Comment
βββ data/ # SQLite database files (gitignored)
βββ .env.example # Environment variable template
βββ go.mod # Go module dependencies
βββ go.sum # Go module checksums
βββ README.md # This file
| Component | Technology | Rationale |
|---|---|---|
| Language | Go 1.25.0 | Performance, concurrency, matching interfacer-dpp |
| Web Framework | Gin HTTP | Fast, lightweight, matching interfacer-dpp |
| Database | SQLite via modernc.org/sqlite |
Pure Go, zero CGO, embedded, self-contained |
| Authentication | Zenroom VM + DID resolution | EdDSA signature verification, identical to interfacer-dpp |
| Identifiers | ULID (github.com/oklog/ulid/v2) |
Sortable, URL-safe, matching interfacer-dpp |
| CORS | github.com/gin-contrib/cors |
Matching interfacer-dpp CORS configuration |
| Env Loading | github.com/joho/godotenv |
Matching interfacer-dpp pattern |
- SQLite over MongoDB: Chosen for this service because feedback data (ratings, comments) is project-scoped and benefits from local transactional integrity. The embedded database eliminates operational complexity while WAL mode ensures adequate concurrent read performance for public API endpoints.
- UPDATE-then-INSERT Upsert: Reviews use an explicit update-then-insert pattern (rather than
INSERT OR REPLACE) to preserve the original ULID and creation timestamp when a user updates their existing review. - Soft Delete for Comments: Deleted comments are marked
status = 'deleted'rather than physically removed, preserving reply thread integrity. Only the comment author can delete their own comment. - Auth Middleware vs Inline: Auth is implemented as Gin middleware (applied per-route), an improvement over interfacer-dpp's inline handler pattern.
GETandOPTIONSrequests skip authentication entirely. - No Nested User Objects: All responses return only
user_ulidβ the frontend is responsible for hydrating user metadata from the Identity service. Contract tests enforce this separation. - Zenroom Mutex: All Zenroom cryptographic calls are serialized through a
sync.Mutexbecause thezencode-execlibrary is not thread-safe.
Before running this application, ensure you have the following installed:
- Go: Version 1.25.0 or higher
- zencode-exec: Zenroom cryptographic VM binary (required for EdDSA signature verification)
- Available from Zenroom.org or via the Dyne.org package repositories
- SQLite: Version 3.x (bundled via
modernc.org/sqlite, no system-level install required) - Docker (optional): For containerized deployment
-
Clone the repository with submodules:
git clone --recurse-submodules https://github.com/interfacerproject/interfacer-feedback-service.git cd interfacer-feedback-service -
Install Go dependencies:
go mod download
-
Configure environment:
cp .env.example .env # Edit .env with your settings if needed -
Ensure zencode-exec is available:
which zencode-exec # If not found, install from https://zenroom.org/ -
Run the application:
go run cmd/main/main.go
The API will be available at http://localhost:8081.
β οΈ Note: The application expects.envtwo directories above the binary (../../.env), matching thecmd/main/layout. When running from the project root, this resolves correctly.
The application is configured through environment variables (loaded from .env via godotenv). Copy .env.example to .env and adjust as needed:
| Variable | Default | Description |
|---|---|---|
SQLITE_PATH |
./data/feedback.db |
Path to SQLite database file |
SERVER_URL |
http://localhost:8081 |
Public base URL of this service |
BASE_DID_URL |
https://did.dyne.org |
Base URL for DID resolution (appended to public key) |
DID_CONTEXT_PATH |
/path/to/did-context.json |
DID context path (e.g., did:dyne:ifacer) |
The application automatically creates the SQLite database file, tables, and indexes on first run. No manual database setup or migration tooling is required.
The database is created with:
- WAL mode (
PRAGMA journal_mode=WAL) for concurrent read performance - Foreign keys enabled (
PRAGMA foreign_keys=ON) for referential integrity - All tables and indexes created via
IF NOT EXISTSidempotent migrations
The service listens on port 8081 by default. To change this, modify the router.Run() call in cmd/main/main.go or set the PORT environment variable.
This service uses the same DID-based EdDSA authentication flow as interfacer-dpp. The frontend/proxy sends signed requests, and the service verifies the signature using Zenroom cryptographic VM and DID resolution.
| Header | Description |
|---|---|
did-sign |
EdDSA signature of the request body (base64-encoded) |
did-pk |
EdDSA public key of the signer |
x-user-id |
ULID of the authenticated user (set by proxy after successful auth) |
- Header Extraction: Extract
did-sign,did-pk, andx-user-idfrom request headers - DID Resolution: Fetch the DID document from
BASE_DID_URLusing the public key β returns 401 if the DID is not found or unreachable - Zenroom Signature Verification: Call
zencode-execwith the embeddedverify_graphql.zenscript, passing the base64-encoded request body, the signature, and the public key β returns 401 on failure - Context Propagation: On success,
user_ulidis set in the Gin context for downstream handlers
| Endpoint Type | Auth Required | Authorization Rule |
|---|---|---|
GET (all) |
No | Public read access |
POST /reviews |
Yes | Any authenticated user; UNIQUE(project_ulid, user_ulid) limits one review per project |
DELETE /reviews/:id |
Yes | Must match review's user_ulid (author-only); returns 403 on mismatch |
POST /comments |
Yes | Any authenticated user |
DELETE /comments/:id |
Yes | Must match comment's user_ulid (author-only); returns 403 on mismatch |
AllowAllOrigins: true
AllowMethods: GET, POST, PUT, DELETE, OPTIONS
AllowHeaders: Origin, Content-Type, Accept, did-sign, did-pk, x-user-id
ExposeHeaders: Content-Length
AllowCredentials: false
MaxAge: 12 hours
http://localhost:8081
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/projects/{project_ulid}/reviews |
Yes | Create or update a review |
GET |
/api/v1/projects/{project_ulid}/reviews |
No | List reviews (paginated) |
GET |
/api/v1/projects/{project_ulid}/reviews/summary |
No | Get aggregated review stats |
GET |
/api/v1/projects/{project_ulid}/reviews/mine |
No* | Get the current user's review (null if none) |
POST |
/api/v1/projects/{project_ulid}/comments |
Yes | Create a comment or reply |
GET |
/api/v1/projects/{project_ulid}/comments |
No | List comments (paginated, threaded) |
DELETE |
/api/v1/reviews/{review_id} |
Yes | Delete a review (author-only) |
DELETE |
/api/v1/comments/{comment_id} |
Yes | Soft-delete a comment (author-only) |
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Maximum results per page (1β100) |
cursor |
integer | β | Unix timestamp of last item from previous page (for cursor pagination) |
Response:
{
"reviews": [
{
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"project_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAB",
"user_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAC",
"rating": 4,
"content": "Great project with excellent documentation!",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
]
}{
"average_rating": 4.33,
"total_reviews": 12,
"rating_distribution": {
"1": 0,
"2": 1,
"3": 2,
"4": 4,
"5": 5
}
}Returns the authenticated user's review for the project, or null if none exists.
Requires the x-user-id header. If not present, returns {"review": null}.
Response (review exists):
{
"review": {
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"project_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAB",
"user_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAC",
"rating": 4,
"content": "Great project with excellent documentation!",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}Response (no review):
{
"review": null
}Permanently deletes a review. Only the review author can delete their own review.
Auth headers (did-sign, did-pk, x-user-id) are required.
Response:
{
"status": "deleted"
}Error (not the author):
{
"error": "review not found or not owned by user"
}| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Maximum results per page (1β100) |
cursor |
integer | β | Unix timestamp of last item from previous page |
parent_id |
string | β | Fetch replies to a specific parent comment (omit for root comments) |
Response:
{
"comments": [
{
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
"project_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAB",
"user_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAC",
"parent_id": null,
"content": "Has anyone tested this with the latest firmware?",
"attachments": null,
"status": "active",
"created_at": "2025-02-01T14:00:00Z",
"updated_at": "2025-02-01T14:00:00Z"
}
]
}All error responses follow the interfacer-dpp convention:
{
"error": "Human-readable error message",
"details": "Optional detailed error information"
}| Status | Meaning |
|---|---|
200 OK |
Successful GET, PUT, DELETE operations |
201 Created |
Successful POST operation |
400 Bad Request |
Invalid request format, missing fields, or rating out of range |
401 Unauthorized |
Missing or invalid authentication headers |
403 Forbidden |
Attempted to delete another user's review or comment |
404 Not Found |
Resource not found |
500 Internal Server Error |
Server-side error |
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
TEXT (ULID) | PRIMARY KEY | Unique review identifier |
project_ulid |
TEXT | NOT NULL, INDEXED | Target project ULID |
user_ulid |
TEXT | NOT NULL, INDEXED | Review author ULID |
rating |
INTEGER | NOT NULL, CHECK 1β5 | Star rating |
content |
TEXT | NULLABLE | Optional review text |
created_at |
INTEGER | NOT NULL | Unix timestamp |
updated_at |
INTEGER | NOT NULL | Unix timestamp |
Unique Constraint: UNIQUE(project_ulid, user_ulid) β ensures one review per user per project. Subsequent POSTs update the existing review.
| Field | Type | Description |
|---|---|---|
average_rating |
FLOAT64 | Mean of all ratings for the project |
total_reviews |
INTEGER | Total number of reviews |
rating_distribution |
OBJECT (intβint) | Count of reviews per rating value (1β5) |
| Field | Type | Constraints | Description |
|---|---|---|---|
id |
TEXT (ULID) | PRIMARY KEY | Unique comment identifier |
project_ulid |
TEXT | NOT NULL, INDEXED | Target project ULID |
user_ulid |
TEXT | NOT NULL, INDEXED | Comment author ULID |
parent_id |
TEXT (ULID) | NULLABLE, INDEXED | Parent comment ULID (null = root comment) |
content |
TEXT | NOT NULL | Comment body text |
attachments |
TEXT (JSON) | NULLABLE | JSON array of file IDs/URLs from storage service |
status |
TEXT | DEFAULT 'active' | Comment status: active or deleted |
created_at |
INTEGER | NOT NULL | Unix timestamp |
updated_at |
INTEGER | NOT NULL | Unix timestamp |
idx_reviews_project_user UNIQUE (project_ulid, user_ulid)
idx_comments_project (project_ulid)
idx_comments_user (user_ulid)
idx_comments_parent (parent_id)
idx_comments_created (created_at)
curl -X POST http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/reviews \
-H "Content-Type: application/json" \
-H "did-sign: <base64-eddsa-signature>" \
-H "did-pk: <eddsa-public-key>" \
-H "x-user-id: 01ARZ3NDEKTSV4RRFFQ69G5FAC" \
-d '{
"rating": 5,
"content": "Excellent project! The documentation is thorough and the API is intuitive."
}'curl -X POST http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/reviews \
-H "Content-Type: application/json" \
-H "did-sign: <base64-eddsa-signature>" \
-H "did-pk: <eddsa-public-key>" \
-H "x-user-id: 01ARZ3NDEKTSV4RRFFQ69G5FAC" \
-d '{
"rating": 4,
"content": "Updated my review after more testing."
}'# First page
curl -X GET "http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/reviews?limit=10"
# Next page using cursor (created_at from last item)
curl -X GET "http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/reviews?limit=10&cursor=1737000000"curl -X GET http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/reviews/summarycurl -X POST http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/comments \
-H "Content-Type: application/json" \
-H "did-sign: <base64-eddsa-signature>" \
-H "did-pk: <eddsa-public-key>" \
-H "x-user-id: 01ARZ3NDEKTSV4RRFFQ69G5FAC" \
-d '{
"content": "Has anyone tested the material compatibility?",
"attachments": "[\"file-ulid-1\", \"file-ulid-2\"]"
}'curl -X POST http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/comments \
-H "Content-Type: application/json" \
-H "did-sign: <base64-eddsa-signature>" \
-H "did-pk: <eddsa-public-key>" \
-H "x-user-id: 01ARZ3NDEKTSV4RRFFQ69G5FAF" \
-d '{
"parent_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
"content": "Yes, I tested with PLA and PETG β works great with both!"
}'# Root-level comments
curl -X GET "http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/comments?limit=20"
# Replies to a specific comment
curl -X GET "http://localhost:8081/api/v1/projects/01ARZ3NDEKTSV4RRFFQ69G5FAB/comments?parent_id=01ARZ3NDEKTSV4RRFFQ69G5FAX&limit=20"curl -X DELETE http://localhost:8081/api/v1/comments/01ARZ3NDEKTSV4RRFFQ69G5FAX \
-H "did-sign: <base64-eddsa-signature>" \
-H "did-pk: <eddsa-public-key>" \
-H "x-user-id: 01ARZ3NDEKTSV4RRFFQ69G5FAC"The service publishes asynchronous events to enable inter-service communication. When a review is created or updated, a RatingUpdated event is emitted so the main Project Service can update its cached aggregated ratings.
{
"event_type": "RatingUpdated",
"project_ulid": "01ARZ3NDEKTSV4RRFFQ69G5FAB",
"average_rating": 4.33,
"total_reviews": 12,
"timestamp": 1737000000
}type Publisher interface {
PublishRatingUpdated(event RatingUpdatedEvent) error
}The current implementation uses an in-process LogPublisher that logs events. To connect to a message broker (RabbitMQ, NATS, Kafka), implement the Publisher interface and instantiate it in cmd/main/main.go:
// Replace the LogPublisher with a real broker implementation:
publisher := &broker.RabbitMQPublisher{URL: os.Getenv("BROKER_URL")}Configure the broker URL in your .env:
BROKER_URL=amqp://localhost:5672POST /reviews
β
βΌ
UpsertReview (SQLite)
β
βΌ (goroutine)
GetReviewSummary (SQLite)
β
βΌ
Publisher.PublishRatingUpdated()
β
βΌ
Message Broker β Project Service
Events are published in a goroutine after the HTTP response is sent, so they do not add latency to the API response.
The project follows Go best practices with a clean architecture aligned with interfacer-dpp conventions:
cmd/: Application entry pointsinternal/: Private application codeauth/: DID-based authentication (Zenroom + DID resolution)database/: SQLite connection, migrations, and repository layerhandler/: HTTP request handlers (Gin controllers)model/: Data structures and domain typesevents/: Event publisher interface and event types
- Models: Add new data structures in
internal/model/model.go - Repositories: Implement data access in
internal/database/ - Handlers: Implement business logic in
internal/handler/ - Routes: Register new endpoints in
cmd/main/main.go - Auth: If the endpoint requires authentication, apply
authMWmiddleware
- Define the event struct in
internal/events/events.go - Add a publish method to the
Publisherinterface - Implement the method in all publisher implementations
- Call the publisher from the appropriate handler
- Follow standard Go formatting (
go fmt) - Use meaningful variable and function names
- Add comments for exported functions and types
- Handle errors appropriately β wrap with context using
fmt.Errorf("context: %w", err) - Use
*stringfor nullable fields (matching interfacer-dpp convention) - Preserve the
gin.H{"error": "...", "details": "..."}error response format
The internal/auth/zenflows-crypto directory is a git submodule pointing to the shared Zenroom scripts repository. To update it:
git submodule update --remote internal/auth/zenflows-cryptoThe project includes unit tests, integration tests, and API contract tests.
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run with coverage
go test -cover ./...
# Run specific package tests
go test ./internal/database/...
go test ./internal/handler/...| Package | Test Type | Coverage |
|---|---|---|
internal/database |
Integration tests (in-memory SQLite) | Upsert logic, pagination, summary aggregation, soft delete, authorization rules |
internal/handler |
Contract tests | Response shape validation (no nested user objects), error response format, route registration |
- Response Shape: Verifies that review and comment JSON responses contain only
user_ulidand no nesteduser,username,name,avatar,email, orprofilefields β enforcing the frontend hydration contract - Error Format: Validates that error responses follow the
{"error": "...", "details": "..."}DPP convention - Route Registration: Basic sanity check that routes compile and return proper content types
We welcome contributions to the Interfacer Feedback & Interaction Service! Please follow these guidelines:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
- Follow Go best practices and coding standards
- Add tests for new functionality
- Update documentation (this README) as needed
- Ensure all tests pass before submitting (
go test ./...) - Maintain response format conventions (
gin.H{"error": "..."}) - Keep the project structure aligned with interfacer-dpp conventions
- Use clear, descriptive commit messages
Follow conventional commits where applicable:
feat: add event publishing for comment creation
fix: resolve cursor pagination edge case with zero timestamps
docs: update README with Docker deployment instructions
test: add integration tests for comment thread retrieval
Copyleft (Ι) 2022β2025 by Dyne.org foundation, Amsterdam
Designed, written and maintained by the Interfacer Project team at Dyne.org.
https://www.interfacerproject.eu/
Interfacer Feedback & Interaction Service
Copyleft (Ι) 2022β2025 Dyne.org foundation
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.