# Upload without a server

Let people upload from a static site, a public form or a prototype with nothing but an upload preset's ID. Files go straight to CalmLens, the uploader sees whether their file was accepted, and you review uploads in the portal. When you need to know who uploaded, or to show uploads to other visitors, [upload with your server](/docs/guides/upload-component) instead.

## 1. Create a preset open to anyone

In [Upload presets](/portal/proj_current/upload-presets), create a preset and choose **Anyone with the preset ID**. Anyone who loads your page can upload through it, so its settings are what protect your usage:

| Setting                           | Value                                                                                                                                                                                                                      |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Daily upload limit                | Required. The most uploads each visitor (IP address) makes per UTC day; further uploads fail with [`429 upload-preset/daily-limit-reached`](/docs/api/assets#post-browser-upload-error-upload-preset-daily-limit-reached). |
| File types and maximum size       | Only what your page expects, such as `image/*` up to 10 MB. Anything else is refused before it's stored.                                                                                                                   |
| Allowed sites                     | Your site's origins, so other websites can't use the preset from a browser. Scripts outside a browser can still send uploads, so keep the daily limit. Leave it empty for mobile apps, which send no `Origin` header.      |
| Show the decision to the uploader | On, so your page can tell people whether their upload was accepted.                                                                                                                                                        |
| Keep content after moderation     | On only if your page shows uploads back. When off, each file is deleted once it's moderated, and its record and report remain.                                                                                             |

The preset ID is safe to put in your page: it can only upload through this preset, into its folder, with its settings. [Upload presets](/docs/guides/upload-presets) explains every setting.

## 2. Add the upload widget

Install `@calmlens/sdk` in your front end and add the upload widget with the preset ID alone:

**React**

```tsx
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/react";

export function PublicPhotoUploader({
  onUploaded,
}: {
  onUploaded: (upload: CalmLensUpload) => void;
}) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      accept="image/*"
      maxFileSize={10 * 1024 * 1024}
      onUploaded={onUploaded}
    />
  );
}
```

**Vue**

```vue
<script setup lang="ts">
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/vue";

const emit = defineEmits<{ uploaded: [upload: CalmLensUpload] }>();
</script>

<template>
  <CalmLensUploader
    upload-preset-id="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
    accept="image/*"
    :max-file-size="10 * 1024 * 1024"
    @uploaded="(upload) => emit('uploaded', upload)"
  />
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/svelte";

  let { onUploaded }: { onUploaded: (upload: CalmLensUpload) => void } = $props();
</script>

<CalmLensUploader
  uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
  accept="image/*"
  maxFileSize={10 * 1024 * 1024}
  {onUploaded}
/>
```

**Plain JavaScript**

```javascript
// Defines <calmlens-uploader>. In plain HTML, write the element in your markup instead:
// <calmlens-uploader upload-preset-id="…" accept="image/*">.
import "@calmlens/sdk/elements";

export function mountPublicPhotoUploader(container, { onUploaded }) {
  const uploader = document.createElement("calmlens-uploader");
  uploader.setAttribute("upload-preset-id", "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P");
  uploader.setAttribute("accept", "image/*");
  uploader.setAttribute("max-file-size", String(10 * 1024 * 1024));
  uploader.addEventListener("calmlens-uploaded", (event) => onUploaded(event.detail));
  container.replaceChildren(uploader);
}
```

In a React Native or Expo app, use the [React Native uploader](/docs/widgets/upload-widget#react-native) with the same settings. To build your own interface, create a `CalmLensAppClient` with the preset ID and call `client.assets.classify()`, which waits for the decision.

## 3. Show the uploader the decision

Each upload returns its asset ID, name and upload time, and its moderation status when the preset shows decisions. The widget turns the status into a message for the uploader:

| Status                 | What the uploader sees                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| `allowed` or `limited` | Their file was accepted.                                                                    |
| `age-restricted`       | Their file was accepted and is shown only to people who can see sensitive content.          |
| `pending`              | Their file was uploaded and is still being checked. The widget checks for up to 15 seconds. |
| `blocked` or `purged`  | Their file can't be used because it breaks your rules.                                      |

The browser never receives the moderation report, metadata or URLs, only the status of its own upload.

## 4. Show the upload back to the uploader

To show the uploader their file, turn on **Keep content after moderation**, set the preset's visibility to public, and render the [media component](/docs/widgets/media-component) with your project ID and the asset ID and status from `onUploaded`. It shows blocked and purged files as unavailable without requesting them.

By default the CDN serves a public file as soon as its upload completes, before moderation decides. Turn on [Block assets until moderation finishes](/portal/proj_current/settings#block-unmoderated-assets) to serve only files that have passed moderation.

Only the uploader's browser knows the asset ID, and a browser can't list uploads, so other visitors can't find the file. Showing uploads to everyone needs somewhere to keep their IDs, which is a server.

## 5. Review uploads

Uploads appear in the preset's folder in the portal, with each one's moderation report. To hear about new uploads and decisions, connect Slack or email in [Webhooks & connectors](/portal/proj_current/connectors).

## When to add a server

Move to [uploading through your server](/docs/guides/upload-component) when you need to:

- Know who uploaded a file, or attach it to a record such as a post, with an `externalId` that only your server can set.
- Show uploads to other visitors, which needs their asset IDs stored somewhere.
- Limit uploads per signed-in user instead of per IP address.
- Publish from a decision your server reads from CalmLens. The status in the browser is for the uploader's feedback only, because people can change what their browser sends.

[Calmform](/docs/examples#calmform-browser-feedback-uploads-javascript) is a static feedback form built this way.
