# Asset permissions

Keep assets `private` when access depends on your application's users, teams, subscriptions or sharing rules. Store the CalmLens asset ID alongside your own ownership and access records. CalmLens authenticates your project API key; your backend decides which of your users may access each asset. Your users do not need CalmLens accounts.

[Project and folder permissions](/docs/guides/project-collaboration#custom-and-folder-scoped-access) restrict what your backend's API key can access. They do not evaluate the identity of an end user signed in to your application. Keep that key on your server and grant only the permissions and folders your integration needs, including **Download assets** for private delivery.

Before choosing a delivery method, [retain the content and configure its delivery eligibility](/docs/guides/delivering-assets#before-you-serve-a-file).

## Check access in your backend

Both delivery options start with the same permission check in your backend. Complete it before issuing a signed URL or fetching content to proxy:

1. Authenticate the caller with your application's session or token.
2. Look up the requested asset in your database, scoped to the caller's tenant and ownership or sharing rules. An asset ID supplied by the browser is not proof of access.
3. Return an error without fetching or signing content if the caller cannot read it. A `404` can avoid revealing that another user's asset exists.
4. Resolve the stored CalmLens asset ID and project from that authorised record, then use one of the delivery methods below.

Apply the same check to originals, previews, thumbnails, downloads and transformed versions. If a caller can choose a lens or transformation, validate that choice against the variants they may access; a thumbnail permission need not grant the original file.

## Choose a delivery method

After the access check succeeds, deliver the permitted resource using one of these two options.

|                                     | Short-lived signed URL                   | Authenticated proxy                                                       |
| ----------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- |
| Download path                       | Client downloads directly from CalmLens  | Your backend fetches and streams the file                                 |
| Application permission check        | Before each link is issued               | On every request reaching your endpoint                                   |
| Permission removed in your database | Existing links remain valid until expiry | The next request can be denied, provided caches cannot bypass the check   |
| Application-specific rendering      | Use CalmLens lenses and transforms       | Can also apply your own annotations or processing                         |
| Moderation and content availability | CalmLens checks CDN requests             | CalmLens checks upstream requests; your own byte cache needs invalidation |

Prefer signed URLs for ordinary delivery when a short access window is acceptable. Use a proxy when you need to check application permissions on every request or render content in ways CalmLens does not provide.

### Option 1: Issue a short-lived signed URL

After your backend grants access, create a [signed URL](/docs/guides/delivering-assets#signed-urls) and return it to the browser. The browser downloads directly from CalmLens, so your backend does not relay the file. Choose the resource and any lens or transforms on the server, and use a lifetime suitable for the content.

**TypeScript Server SDK**

```typescript
const signedUrl = await client.assets.createSignedUrl({
  assetId,
  resource: "content",
  expiresInSeconds: 900,
});
```

**JavaScript Server SDK**

```javascript
const signedUrl = await client.assets.createSignedUrl({
  assetId,
  resource: "content",
  expiresInSeconds: 900,
});
```

**python**

```python
import json

options = json.loads("{\"resource\":\"content\",\"expiresInSeconds\":900,\"assetId\":\"your-asset-id\"}")
response = client.assets.create_signed_url(options)
print(response)
```

**go**

```go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {

    var options calmlens.SignedURLOptions
    if err := json.Unmarshal([]byte("{\"resource\":\"content\",\"expiresInSeconds\":900}"), &options); err != nil { log.Fatal(err) }
    options.AssetID = "your-asset-id"
    response, err := client.Assets.CreateSignedURL(context.Background(), options)
    if err != nil { log.Fatal(err) }
    fmt.Println(response)
}
```

**ruby**

```ruby
result = client.assets.create_signed_url(resource: "content", expires_in_seconds: 900, asset_id: "your-asset-id")
puts result
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let result = client.assets().create_signed_url(SignedUrlOptions { resource: Some("content".into()), expires_in_seconds: Some(900), asset_id: "your-asset-id".into(), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;

public class Example {
  public static void example(CalmLensServerClient client) {
    var result = client.assets.createSignedUrl(new SignedUrlOptions().resource("content").expiresInSeconds(900L).assetId("your-asset-id"));
    System.out.println(result);
  }
}
```

**curl**

```bash
curl -X POST "https://api.calmlens.com/v1/assets/$ASSET_ID/signed-url" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "resource": "content",
  "expiresInSeconds": 900
}'
```

The response contains `url` and `expiresAt`. Return them to the client and request a new link through your backend when needed, repeating the permission check. Lifetimes range from 1 second to 24 hours, with a 15-minute default. See [`POST /assets/:assetId/signed-url`](/docs/api/cdn#create-asset-signed-url) for the full contract.

A signed URL is a bearer link: anyone holding it can use it until it expires, subject to CalmLens delivery eligibility. Your application server must check its own permission records before issuing each link. If you remove a user's access in your database, your server should refuse to issue them new links; CalmLens does not read that database or enforce those application permissions. Links already issued remain valid until they expire. Use short lifetimes, or the authenticated proxy when permission must be rechecked on every request.

### Option 2: Proxy content through your application

Use an authenticated endpoint in your own backend when you want to recheck permission on every request. After the access check, fetch the private resource with your server SDK and stream its response to the caller:

**TypeScript Server SDK**

```typescript
const response = await client.cdn.serve({
  assetIdentifier: assetId,
});
```

**python**

```python
with client.cdn.serve({"assetIdentifier": "your-asset-id"}) as response:
    if not 200 <= response.status < 300:
        raise RuntimeError(f"CDN returned HTTP {response.status}")
    content = response.read()
print(len(content))
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "io"
    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {
    ctx := context.Background()
    response, err := client.CDN.Serve(ctx, calmlens.CDNServeOptions{CDNAssetURLOptions: calmlens.CDNAssetURLOptions{AssetIdentifier: "your-asset-id"}})
    if err != nil { log.Fatal(err) }
    defer response.Body.Close()
    if response.StatusCode < 200 || response.StatusCode >= 300 { log.Fatalf("CDN returned HTTP %d", response.StatusCode) }
    content, err := io.ReadAll(response.Body)
    if err != nil { log.Fatal(err) }
    fmt.Println(len(content))
}
```

**JavaScript Server SDK**

```javascript
const response = await client.cdn.serve({
  assetIdentifier: assetId,
});
```

**ruby**

```ruby
result = client.cdn.serve(asset_identifier: "your-asset-id")
puts result
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let result = client.cdn().serve(CdnRequest { asset_identifier: "your-asset-id".into(), ..Default::default() }).await?;
    println!("{}", result.status());
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;

public class Example {
  public static void example(CalmLensServerClient client) {
    try (var result = client.cdn.serve(new CdnRequest().assetIdentifier("your-asset-id"))) {
      System.out.println(result.status());
    }
  }
}
```

**curl**

```bash
curl "https://cdn.calmlens.com/v1/assets/$CALMLENS_PROJECT_ID/$ASSET_ID/content"
```

Check the upstream status before returning its body, and preserve the appropriate content type. The browser receives your application's content endpoint, while the CalmLens API key stays on the server.

PolySnap uses this pattern: its asset-content route authenticates the caller, queries its project asset records by both asset ID and user ID, returns `404` if there is no owned asset, and only then calls CalmLens through a server-side content helper. That helper can also apply an application-selected lens or image edits.

Run authorisation before looking up a cached response, including cache hits. Keep any internal byte cache behind that check. For private responses sent to clients, use `Cache-Control: private, no-store` when each new request must reach your access check; do not forward public cache headers or allow a shared cache to serve your authenticated endpoint without authorisation. If your application caches content independently, you must also handle moderation changes, content removal and cache invalidation; CalmLens cannot check or revoke bytes served from your cache.
