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
Binary file added .github/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 76 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,98 @@
<div align="center">

<img src=".github/assets/logo.png" alt="Nvisy Server" width="104" height="104" />

# Nvisy Server

[![Build](https://img.shields.io/github/actions/workflow/status/nvisycom/server/build.yml?branch=main&label=build%20%26%20test&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/build.yml)
**Detect and redact sensitive data across documents, images, and audio.**

The open-source multimodal redaction API: an LLM-powered engine and HTTP service
that finds PII and applies your redaction policies, wrapped in a multi-tenant,
self-hostable Rust server.

[![Build](https://img.shields.io/github/actions/workflow/status/nvisycom/server/build.yml?branch=main&label=build&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/build.yml)
[![Release](https://img.shields.io/github/actions/workflow/status/nvisycom/server/release.yml?branch=main&label=release&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/release.yml)
[![Security](https://img.shields.io/github/actions/workflow/status/nvisycom/server/security.yml?branch=main&label=security&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/security.yml)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE.txt)

Open-source multimodal redaction API. Detect and redact PII and sensitive data
across documents, images, and audio.
[**nvisy.com**](https://nvisy.com) · [**docs.nvisy.com**](https://docs.nvisy.com) · [**app.nvisy.com**](https://app.nvisy.com)

</div>

A document flows through two phases: **detection** analyzes it for sensitive
entities and stores a reviewable report; **redaction** applies the pipeline's
policies (with optional reviewer edits) to produce a redacted file. Detection
runs asynchronously off a transactional work queue; redaction is synchronous and
repeatable. Everything is scoped to isolated workspaces with per-workspace
credential encryption.

> [!WARNING]
> **Active development: API not stable.** This project is under active
> development. Public APIs, configuration shapes, on-disk formats, and
> wire protocols may change without notice between releases. Pin a
> specific commit if you depend on this in production.
> **Active development. API not stable.** Public APIs, configuration shapes,
> on-disk formats, and wire protocols may change without notice between releases.
> Pin a specific commit if you depend on this in production.

## Features

- **Multimodal Redaction:** Detect and remove sensitive data across PDFs, images, and audio
- **AI-Powered Detection:** LLM-driven PII and entity recognition with configurable redaction policies
- **Workspace Isolation:** Multi-tenant workspaces with HKDF-derived credential encryption
- **Real-Time Collaboration:** WebSocket and NATS pub/sub for live document editing
- **Interactive Docs:** Auto-generated OpenAPI with Scalar UI
- **Multimodal redaction** — detect and remove sensitive data across PDFs, office documents, images, and audio.
- **AI-powered detection** — LLM- and pattern-driven PII/entity recognition, governed by configurable redaction policies.
- **Reviewer edits** — suppress a false positive, retag a detection, or add one the analysis missed, then re-redact — as many times as needed.
- **Workspace isolation** — multi-tenant workspaces with HKDF-derived, per-workspace credential encryption.
- **Real-time collaboration** — WebSocket and NATS pub/sub for live status and document editing.
- **Interactive docs** — auto-generated OpenAPI served through a Scalar UI.

## Requirements

## Quick Start
- **Rust + Cargo** — 1.95+, Edition 2024
- **PostgreSQL** 18+ and **NATS** 2.10+ (JetStream) — the dev compose file provides both

The fastest way to get started is with [Nvisy Cloud](https://nvisy.com).
## Quick start

For self-hosted deployments, refer to [`docker/`](docker/) for compose files and
infrastructure requirements, and [`.env.example`](.env.example) for configuration.
The fastest way to get started is with [Nvisy Cloud](https://nvisy.com). To run a
server locally:

```bash
make install-all # Install tools and make scripts executable
make generate-all # Generate .env, auth keys, and apply migrations

docker compose -f docker/docker-compose.dev.yml up -d # Start Postgres + NATS
make run # Run the server
```

The API then serves interactive OpenAPI docs (Scalar UI) at the running server's
docs path. For self-hosted deployments, see [`docker/`](docker/) for compose
files and infrastructure requirements, and [`.env.example`](.env.example) for
configuration.

## Commands

| Command | What it does |
| --- | --- |
| `make run` | Run the server (starts Postgres and NATS first) |
| `make ci` | Run all CI checks locally (check, fmt, clippy, test, docs) |
| `make fmt` | Fix code formatting (nightly rustfmt) |
| `make security` | Run security checks (`cargo deny`) |
| `make generate-migrations` | Apply migrations and regenerate `schema.rs` |
| `make reset-docker` | Reset the dev containers (`down -v`, then `up -d`) |

## Documentation

See [`docs/`](docs/) for architecture, intelligence capabilities, provider
design, and security documentation.
See [`docs/`](docs/) for the details:

- [Architecture](docs/ARCHITECTURE.md) — the crates, the detect/redact pipeline, and how they fit together.
- [Intelligence](docs/INTELLIGENCE.md) — detection capabilities and the redaction engine.
- [Providers](docs/PROVIDERS.md) — inference and object-store provider design.
- [Security](docs/SECURITY.md) — the encryption, authentication, and isolation model.

## Changelog
## Contributing

See [CHANGELOG.md](CHANGELOG.md) for release notes and version history.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines, and
[CHANGELOG.md](CHANGELOG.md) for release notes.

## License

Apache 2.0 License, see [LICENSE.txt](LICENSE.txt)
Apache 2.0 License, see [LICENSE.txt](LICENSE.txt).

## Support

- **Documentation:** [docs.nvisy.com](https://docs.nvisy.com)
- **Issues:** [GitHub Issues](https://github.com/nvisycom/server/issues)
- **Email:** [support@nvisy.com](mailto:support@nvisy.com)
- **API Status:** [nvisy.openstatus.dev](https://nvisy.openstatus.dev)
- **Documentation**: [docs.nvisy.com](https://docs.nvisy.com)
- **Email**: [support@nvisy.com](mailto:support@nvisy.com)
3 changes: 2 additions & 1 deletion crates/nvisy-server/src/extract/auth/auth_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ where
T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static,
{
fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) {
// Add security requirement for Bearer token
// The Bearer token is required: the only way to satisfy the operation is
// to present it.
operation.security = vec![[("BearerAuth".to_string(), vec![])].into()];
}
}
2 changes: 2 additions & 0 deletions crates/nvisy-server/src/extract/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod auth_provider;
mod auth_state;
mod jwt_claims;
mod jwt_header;
mod optional_auth;
mod permission;

use uuid::Uuid;
Expand All @@ -16,6 +17,7 @@ pub use self::auth_provider::AuthProvider;
pub use self::auth_state::AuthState;
pub use self::jwt_claims::AuthClaims;
pub use self::jwt_header::AuthHeader;
pub use self::optional_auth::OptionalAuth;
pub use self::permission::{AuthResult, Permission};

impl<T> AuthProvider for AuthClaims<T> {
Expand Down
91 changes: 91 additions & 0 deletions crates/nvisy-server/src/extract/auth/optional_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Optional authentication extractor for endpoints that vary by whether a caller
//! authenticated, without marking themselves auth-required in the OpenAPI spec.

use aide::OperationInput;
use aide::generate::GenContext;
use aide::openapi::{Operation, SecurityRequirement};
use axum::extract::{FromRef, FromRequestParts, OptionalFromRequestParts};
use axum::http::request::Parts;
use derive_more::{Deref, DerefMut};
use nvisy_postgres::PgClient;
use serde::Deserialize;

use super::AuthState;
use crate::handler::{Error, Result};
use crate::service::SessionKeys;

/// Optional [`AuthState`] for an endpoint that runs with or without a token.
///
/// Extracting a bare `Option<AuthState>` authenticates the same way, but its
/// generated OpenAPI security comes from the blanket `Option<T>` `OperationInput`,
/// which delegates to [`AuthState`] and so wrongly marks the operation
/// auth-required. This wrapper carries the same optional value while declaring the
/// token as *optional* in the spec (an empty requirement alongside the Bearer one,
/// so a public probe is not shown as needing a token). Use it for endpoints that
/// vary their behavior by whether a caller authenticated — e.g. the health check.
#[derive(Debug, Clone, Deref, DerefMut)]
pub struct OptionalAuth<T = ()>(pub Option<AuthState<T>>);

impl<T, S> FromRequestParts<S> for OptionalAuth<T>
where
T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static,
S: Sync + Send + 'static,
PgClient: FromRef<S>,
SessionKeys: FromRef<S>,
{
type Rejection = Error<'static>;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Reuses the optional extraction: a valid token authenticates, an absent or
// invalid one yields `None` rather than rejecting.
<AuthState<T> as OptionalFromRequestParts<S>>::from_request_parts(parts, state)
.await
.map(OptionalAuth)
}
}

impl<T> OperationInput for OptionalAuth<T>
where
T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static,
{
fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) {
// Two alternatives: an empty requirement (no auth) and the Bearer one, so
// the operation is documented as accessible with or without a token.
operation.security = vec![
SecurityRequirement::new(),
[("BearerAuth".to_string(), vec![])].into(),
];
}
}

#[cfg(test)]
mod tests {
use aide::OperationInput;
use aide::openapi::Operation;

use super::OptionalAuth;

/// The OpenAPI security for an optional-auth operation must offer an
/// unauthenticated alternative (an empty requirement) alongside the Bearer
/// one, so the endpoint is not documented as requiring a token — a public
/// probe hitting the health check must not appear to need credentials.
#[test]
fn documents_auth_as_optional_not_required() {
let mut operation = Operation::default();
aide::generate::in_context(|ctx| {
OptionalAuth::<()>::operation_input(ctx, &mut operation);
});

assert!(
operation.security.iter().any(|req| req.is_empty()),
"an empty requirement must be present so no auth also satisfies the operation",
);
assert!(
operation
.security
.iter()
.any(|req| req.contains_key("BearerAuth")),
"the Bearer alternative must still be offered for authenticated callers",
);
}
}
2 changes: 1 addition & 1 deletion crates/nvisy-server/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod version;
mod workspace_context;

pub use crate::extract::auth::{
AuthClaims, AuthHeader, AuthProvider, AuthResult, AuthState, Permission,
AuthClaims, AuthHeader, AuthProvider, AuthResult, AuthState, OptionalAuth, Permission,
};
pub use crate::extract::avatar::Avatar;
pub use crate::extract::connection_info::{AppConnectInfo, ClientIp};
Expand Down
4 changes: 2 additions & 2 deletions crates/nvisy-server/src/handler/monitors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use axum::http::StatusCode;
use nvisy_core::health::HealthStatus;

use super::response::Health;
use crate::extract::{AuthState, Json, Version};
use crate::extract::{Json, OptionalAuth, Version};
use crate::handler::Result;
use crate::service::{HealthCache, ServiceState};

Expand Down Expand Up @@ -43,7 +43,7 @@ const TRACING_TARGET: &str = "nvisy_server::handler::monitors";
)]
async fn health_status(
State(health_service): State<HealthCache>,
auth_state: Option<AuthState>,
OptionalAuth(auth_state): OptionalAuth,
version: Version,
) -> Result<(StatusCode, Json<Health>)> {
let is_authenticated = auth_state.is_some();
Expand Down