Skip to content
105 changes: 105 additions & 0 deletions content/docs/get-started/choose-your-stack.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Choose your stack
description: "Pick an integration path: SDK or Proxy, your Postgres platform, your ORM, and your identity provider."
type: guide
components: [encryption, proxy, eql, platform]
audience: [developer, cto]
---

Four decisions, in order. The first one matters most; the rest usually follow from what you already run.

## 1. SDK or Proxy

| | [Stack SDK](/reference/stack) | [Proxy](/reference/proxy) |
|---|---|---|
| Where encryption happens | In your application process | In the proxy, which you run |
| Application changes | Encrypt and decrypt calls, or an ORM wrapper that adds them for you | Point your connection string at the proxy |
| Best when | You own the code and want fine-grained control | You cannot change the application, or it isn't TypeScript |
| Language | TypeScript / JavaScript | Any. It speaks the Postgres wire protocol |
| Runs where | Your app | A container or sidecar in your infrastructure |

Both produce the same ciphertext and both speak [EQL](/reference/eql), so this is not a one-way door. You can move a table from one to the other without re-encrypting it.

Start with the SDK unless you already know you can't change the application.

## 2. Your Postgres

EQL installs into any Postgres you can connect to. It needs no extension, no superuser, and no `postgresql.conf` changes.

| Platform | Works | Notes |
|---|---|---|
| Supabase | Yes | [Supabase integration](/integrations/supabase). Expose the `eql_v3` schema in the dashboard. |
| Neon, RDS, Aurora, Cloud SQL | Yes | `stash eql install` detects a non-superuser role and adapts. |
| Self-hosted Postgres | Yes | Full operator-class support, so the ORE ordering mechanism is available. |
| Postgres behind PgBouncer | Yes | Transaction pooling is fine. Proxy has its own pooling. |
| DynamoDB | Yes, without EQL | [DynamoDB integration](/stack/cipherstash/encryption/dynamodb). Encrypted attributes and HMAC key lookups; no EQL, because there's no Postgres. |
| MySQL, MongoDB, others | Not yet | The SDK still encrypts values for you, but nothing is queryable server-side. |

<Callout type="info">
Managed platforms usually block custom operator class creation. That's why the default ordering mechanism is moving to OPE, which indexes under Postgres's default btree operator class. See [SEM specifiers](/reference/eql/core-concepts#sem-specifiers).
</Callout>

## 3. Your ORM

| Query layer | Path | What it looks like |
|---|---|---|
| Drizzle | [Drizzle integration](/stack/cipherstash/encryption/drizzle) | An `encryptedType` column and typed query operators. Your Drizzle code keeps its shape. |
| Supabase JS | [Supabase integration](/integrations/supabase) | `encryptedSupabase` wraps the client. `.eq()`, `.like()`, `.gt()` keep working. |
| Prisma | [Prisma integration](/stack/cipherstash/encryption/prisma-next) | Encrypt in the data layer around Prisma calls. |
| Raw SQL | [Quickstart](/get-started/quickstart) | Encrypt the value, insert it, cast the query operand. |
| An ORM we don't list | The SDK | Encrypt before write, decrypt after read. Every ORM supports that. |

If you use no ORM, nothing is missing. The SDK's `encrypt` and `encryptQuery` are the whole surface.

## 4. Identity binding

Reach for this when a value should be decryptable only by the user it belongs to, rather than by anything holding the application's credentials. It is also what shrinks the blast radius of a compromised application process.

It requires an OIDC provider, registered on the [OIDC providers](https://dashboard.cipherstash.com/workspaces/_/oidc-providers) page in the Dashboard.

| Provider | Supported |
|---|---|
| Supabase Auth | Yes |
| Clerk | Yes |
| Auth0 | Yes |
| Okta | Yes |
| Any OIDC issuer | Yes, if it publishes a discovery endpoint |

See [provable access control](/solutions/provable-access) for what this buys you, and what it does not.

## Where CipherStash sits among the encryption you already have

Encryption at rest and in transit are not alternatives to this. They protect different things, and you should keep both.

| | At rest | In transit | In use |
|---|---|---|---|
| Protects the disk | Yes | No | Yes |
| Protects the wire | No | Yes | Yes |
| Protects during a query | No | No | Yes |
| Plaintext visible to the database | Yes | Yes | **No** |
| Queryable without decrypting | n/a | n/a | Yes |

A database breach defeats encryption at rest, because the database decrypts on read. That is the gap this fills.

## From development to production

| Stage | Credentials | Setup |
|---|---|---|
| Local development | Device-based | `stash init` per developer. No environment variables. |
| CI and staging | Machine | An application client key, plus `CS_*` environment variables. |
| Production | Machine | Same, with the client key held in your secret manager. |

Device auth gives each developer their own identity, so local decryptions are attributable. There's no interactive device in CI, hence the switch. See [going to production](/stack/deploy/going-to-production).

## Still not sure

A [discovery session](https://cipherstash.com/contact) is a 60-minute conversation with our engineering team. No slides. Bring whoever owns the data layer, and a compliance lead if that's someone else.

Worth thinking through beforehand:

- Which fields are sensitive, and which regulations cover them.
- Whether you need those fields **searchable**, or only stored and retrieved. Encrypt-only columns carry no index terms and leak nothing.
- Whether you need per-user decryption, or per-tenant isolation, or both.
- Your database, ORM, and deployment target, plus any restriction on native Node modules.

You'll leave with a recommended path and answers to whatever is blocking you.
21 changes: 18 additions & 3 deletions content/docs/get-started/index.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
---
title: Get started
description: "What CipherStash is, a 10-minute quickstart, and how to choose your integration path."
navTitle: Overview
description: "Encrypt your first field in ten minutes, then pick the integration path that matches your stack."
type: tutorial
audience: [developer]
---

This section is being built as part of the docs V2 overhaul ([CIP-3307](https://linear.app/cipherstash/issue/CIP-3307)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).
CipherStash encrypts your data at the field level, in your application, and keeps the ciphertext queryable in Postgres. The database never sees plaintext, and neither do we.

Until it lands, current documentation lives in the [existing docs](/stack).
This section gets you from nothing to an encrypted, queryable field. It owns no mechanics: each page links to the reference that does.

## Start here

- **[What is CipherStash?](/get-started/what-is-cipherstash)** is the mental model. What the pieces are, what the trade is, and which door to walk through.
- **[Quickstart](/get-started/quickstart)** is ten minutes of typing. Encrypt a field, store it, query it without decrypting it, and read it back. It works against any Postgres: Supabase, Neon, RDS, or a container.
- **[Choose your stack](/get-started/choose-your-stack)** picks your integration path: SDK or Proxy, your platform, your ORM, and your identity provider.

## Then pick your path

- **Adopting CipherStash into an existing app?** [Integrations](/integrations) covers the platforms, ORMs, frameworks, and auth providers that wrap your existing client, so your queries keep the shape they already have.
- **Can't change the application?** [CipherStash Proxy](/reference/proxy) sits in front of Postgres and encrypts on the wire.
- **Want to understand it before you install it?** [Concepts](/concepts) explains searchable encryption, key management, and identity-aware encryption.
- **Evaluating for a security review?** [Architecture & security](/security) is written to be read end to end, starting with [Cryptography](/security/cryptography).
8 changes: 7 additions & 1 deletion content/docs/get-started/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"title": "Get started",
"icon": "Rocket",
"pages": ["..."]
"pages": [
"index",
"what-is-cipherstash",
"quickstart",
"choose-your-stack",
"..."
]
}
203 changes: 203 additions & 0 deletions content/docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
title: Quickstart
description: "Encrypt, store, query, and decrypt your first field in Postgres, using the stash CLI and @cipherstash/stack."
type: tutorial
components: [encryption, eql, cli, platform]
audience: [developer]
verifiedAgainst:
stack: "0.19.0"
cli: "0.17.1"
eql: "3.0.0"
---

CipherStash encrypts your data at the field level. Every value gets its own encryption key, and the database never sees plaintext. A breach, a compromised agent, or a curious insider sees ciphertext.

By the end of this page you will have encrypted a field, stored it in Postgres, queried it **without decrypting it**, and read it back. It takes about ten minutes and works with any Postgres: Supabase, Neon, RDS, or a Docker container.

## Before you start

- Node.js 18 or later.
- A Postgres database you can connect to.
- A [CipherStash account](https://dashboard.cipherstash.com/sign-up). The free plan is enough.

<Steps>

<Step>

## Initialize your project

```bash cta cta-type="quickstart" example-id="quickstart-stash-init"
npx stash init
```

This opens a browser for device-based authentication, so local development needs no environment variables and no shared secrets. `init` then:

1. Connects to your workspace.
2. Resolves your database connection and runs `stash eql install`, which installs [EQL](/reference/eql), the Postgres surface that makes encrypted columns queryable.
3. Installs `@cipherstash/stack` if it isn't already present.
4. Scaffolds an encryption module at `src/encryption/index.ts`.
5. Writes `.cipherstash/context.json` with what it detected about your project.

See the [CLI reference](/reference/cli/init) for the flags, including `--supabase`, `--drizzle`, and `--prisma-next`.

</Step>

<Step>

## Install EQL v3

This guide, and the rest of the EQL reference, targets **EQL v3**. `stash init` installs EQL v2 by default today, so ask for v3 explicitly:

```bash example-id="quickstart-eql-v3"
npx stash eql install --eql-version 3 --force
```

`--force` reinstalls over the v2 schema that `init` just laid down. On a new database with no encrypted data, that is safe: there is nothing to migrate.

Check what you ended up with:

```bash
npx stash eql status
```

<Callout type="info">
`--eql-version` accepts `2` or `3` and **defaults to `2`** in `stash` 0.17.1. The default becomes `3` in a future release, at which point this step goes away. Pass the flag until then, so you know which generation you are on rather than inheriting a default that is about to change.

v3 installs directly against the database, so the `--drizzle`, `--migration`, and `--latest` flags do not apply to it. See [installing EQL](/reference/eql) for the full options, including installing across several databases.
</Callout>

</Step>

<Step>

## Define what to encrypt

`init` scaffolds `src/encryption/index.ts`. It declares which columns are encrypted, and exports a ready-to-use client. Edit it to match your table:

```typescript filename="src/encryption/index.ts"
import { Encryption, encryptedTable, encryptedColumn } from "@cipherstash/stack"

export const users = encryptedTable("users", {
email: encryptedColumn("email").equality(),
})

export const encryptionClient = await Encryption({ schemas: [users] })
```

`.equality()` declares a **capability**: it is what makes `WHERE email = ?` work later. Add `.orderAndRange()` for `ORDER BY` and comparisons, or `.freeTextSearch()` for token containment. Declare only the capabilities you query on, because each one stores an extra index term alongside the ciphertext.

<Callout type="info">
Prefer to have an agent do this? `npx stash plan` inspects your project and drafts `.cipherstash/plan.md` listing the columns worth encrypting. Review it, then `npx stash impl` applies it. This page does it by hand so you can see each moving part.
</Callout>

</Step>

<Step>

## Create the encrypted column

An encrypted column is typed with an EQL domain. The domain you pick has to match the capability you declared: `.equality()` on a text column means `public.text_eq`.

```sql filename="schema.sql"
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email public.text_eq
);
```

The type is what fixes the column's searchable surface. There is no separate configuration table. See [core concepts](/reference/eql/core-concepts) for the full variant model.

</Step>

<Step>

## Encrypt a value and store it

```typescript filename="src/encrypt.ts"
import { encryptionClient, users } from "./encryption"

const encrypted = await encryptionClient.encrypt("alice@example.com", {
column: users.email,
table: users,
})

if (encrypted.failure) {
throw new Error(`Encryption failed: ${encrypted.failure.message}`)
}

await db.query("INSERT INTO users (email) VALUES ($1)", [encrypted.data])
```

Every operation returns a `Result`: either `data` or `failure`, never both. Encryption happens in your process, so the plaintext never leaves it.

</Step>

<Step>

## Query without decrypting

Encrypt the search term, then use it in an ordinary `WHERE` clause.

```typescript filename="src/query.ts"
import { encryptionClient, users } from "./encryption"

const term = await encryptionClient.encryptQuery("alice@example.com", {
column: users.email,
table: users,
})

if (term.failure) {
throw new Error(`Query encryption failed: ${term.failure.message}`)
}

const rows = await db.query(
"SELECT id, email FROM users WHERE email = $1::public.text_eq",
[term.data],
)
```

The cast matters. An encrypted operator only resolves against a **typed operand**, so `$1::public.text_eq` is what tells Postgres to compare encrypted terms rather than fall back to raw `jsonb` semantics. See [typed operands](/reference/eql/core-concepts).

Postgres compares ciphertext against ciphertext. It never sees either plaintext.

<Callout type="warn">
`LIKE` and `ILIKE` do not work on encrypted columns, in any variant. Free-text matching uses bloom-filter token containment (`@>`) on a `text_match` or `text_search` column instead. See [text](/reference/eql/text).
</Callout>

</Step>

<Step>

## Decrypt

```typescript filename="src/decrypt.ts"
const decrypted = await encryptionClient.decrypt(row.email)

if (decrypted.failure) {
throw new Error(`Decryption failed: ${decrypted.failure.message}`)
}

console.log(decrypted.data) // "alice@example.com"
```

`decrypt` takes the encrypted payload and nothing else. The payload carries the key ID it needs.

</Step>

</Steps>

## What you just built

You encrypted a field, stored it, queried it without decrypting, and read it back. Underneath:

- **ZeroKMS** returned a key seed, which your application processed with its client key to derive a **unique data key for that value**. The data key existed in your memory for one operation and was then discarded. See [cryptography](/security/cryptography).
- **Your client key** never left your infrastructure. Without it, nothing decrypts, including CipherStash.
- **Your keyset** is the isolation boundary. Data encrypted under one keyset cannot be decrypted with another, which is how you separate tenants or environments.
- **The index term** stored alongside the ciphertext is what let Postgres answer the query. Each capability you declare has a bounded, published leakage profile: an equality term lets an observer see which rows share a value, and nothing more. Declare only the capabilities you query on, so that what a column reveals stays within what your threat model already tolerates. See [searchable encryption](/concepts/searchable-encryption).

## Next steps

- **Using an ORM or Supabase?** The [Drizzle](/stack/cipherstash/encryption/drizzle) and [Supabase](/integrations/supabase) integrations wrap your existing client so queries look normal.
- **No app changes possible?** [CipherStash Proxy](/reference/proxy) sits in front of Postgres and does this transparently.
- **Bind decryption to a user.** [Provable access control](/solutions/provable-access) ties a value to the identity that encrypted it.
- **Ready to deploy?** [Going to production](/stack/deploy/going-to-production) covers the switch from device auth to machine credentials.
Loading