# Store Jodit uploads in S3: the new connector adapter and Jodit Cloud file storage

Two things landed this week around file storage for the Jodit editor. The Node.js connector, `jodit-nodejs`, now ships an S3 adapter, so a source can point at a bucket instead of a directory. And Jodit Cloud got a managed file browser: connect your own bucket in the cabinet, and the editor loaded from `cloud.xdsoft.net` comes with the uploader and the file browser already wired up.

## The S3 adapter in jodit-nodejs

Until 1.0.37 the connector stored files on the local filesystem, and anything else meant writing a `StorageAdapter` yourself. That is not hard, but nobody enjoys emulating folders on top of S3 keys twice, so the adapter is now part of the package. It is built on the AWS SDK and talks to every service that implements the S3 API: Amazon S3, MinIO, Cloudflare R2, Yandex Object Storage, DigitalOcean Spaces, Backblaze B2.

A source backed by a bucket looks like this:

```typescript
import { start } from 'jodit-nodejs';

await start({
    port: 8081,
    config: {
        allowCrossOrigin: true,
        sources: {
            media: {
                name: 'media',
                title: 'Media library',
                baseurl: 'https://my-bucket.s3.eu-central-1.amazonaws.com/media/',
                storageAdapter: 's3',
                s3: {
                    bucket: 'my-bucket',
                    region: 'eu-central-1',
                    prefix: 'media'
                }
            }
        }
    }
});
```

There is no `root` here. Remote sources get a virtual root and every path the editor sends is confined to the prefix. Credentials are not in the config either: when `s3.credentials` is omitted, the AWS SDK looks in the usual places (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, a profile, an instance role). A Docker run is the environment plus a config file:

```bash
docker run --rm -p 8081:8081 \
  -e AWS_ACCESS_KEY_ID=AKIA... \
  -e AWS_SECRET_ACCESS_KEY=... \
  -v $(pwd)/config.json:/usr/src/app/config.json \
  xdsoft/jodit-nodejs
```

The same source in `config.json`:

```json
{
    "allowCrossOrigin": true,
    "sources": {
        "media": {
            "name": "media",
            "title": "Media library",
            "baseurl": "https://my-bucket.s3.eu-central-1.amazonaws.com/media/",
            "storageAdapter": "s3",
            "s3": { "bucket": "my-bucket", "region": "eu-central-1", "prefix": "media" }
        }
    }
}
```

For an S3-compatible service add `endpoint`, and `forcePathStyle` where the service wants path-style URLs. MinIO for local development:

```typescript
s3: {
    bucket: 'jodit',
    endpoint: 'http://localhost:9000',
    forcePathStyle: true,
    prefix: 'files',
    credentials: { accessKeyId: 'minioadmin', secretAccessKey: 'minioadmin' }
}
```

### How the bucket is laid out

S3 has keys, not folders, so the adapter follows the convention the AWS console uses. A file shown as `/photos/cat.jpg` is the object `media/photos/cat.jpg`. Creating a folder writes a zero-byte object `media/photos/`; a folder that only exists because objects sit under it is listed as well. Thumbnails go to a `_thumbs` folder next to the originals, the same as on disk, so the file browser behaves as before. Rename and move are copy plus delete, and removing a folder deletes everything under the prefix in batches.

### What the bucket needs

The editor loads images by URL, so the prefix has to be readable by browsers. Either a bucket policy that allows `s3:GetObject` on `my-bucket/media/*`, or a CDN in front of a private bucket with `baseurl` pointing at the CDN. The adapter never sets object ACLs; buckets created after 2023 have them disabled by default anyway.

The IAM user of the connector needs `s3:ListBucket` on the bucket and `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject` on the prefix:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-bucket",
            "Condition": { "StringLike": { "s3:prefix": ["media/*", "media"] } }
        },
        {
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
            "Resource": "arn:aws:s3:::my-bucket/media/*"
        }
    ]
}
```

The full reference, including a table of endpoints for MinIO, R2, Yandex, Spaces and B2, is in the connector docs: https://jodit.github.io/jodit-nodejs/aws-s3/

## One connector, many tenants

The second addition to the connector is `resolveSources`. Instead of a fixed list of sources at startup, a callback looks at each request and returns the sources for that tenant. A SaaS that keeps one bucket (or one prefix) per customer can serve all of them from a single process:

```typescript
import { start, type ResolvedSources } from 'jodit-nodejs';

await start({
    config: {
        allowCrossOrigin: true,
        resolveSources: async (req): Promise<ResolvedSources | null> => {
            const tenant = await findTenant(req.header('x-tenant-id'));
            if (tenant === null) {
                return null; // fall back to the static sources
            }

            return {
                id: `${tenant.id}:${tenant.updatedAt}`, // cache key
                sources: {
                    files: {
                        name: 'files',
                        baseurl: tenant.publicUrl,
                        storageAdapter: 's3',
                        s3: {
                            bucket: tenant.bucket,
                            prefix: tenant.prefix,
                            credentials: tenant.credentials
                        }
                    }
                }
            };
        }
    }
});
```

Resolved sources are cached by `id` (200 tenants, 60 seconds by default), and the resolver runs before `checkAuthentication`, so the same lookup can drive the role. There is also `allowedOrigins`, a CORS predicate that sees the request, for restricting origins per tenant. Details: https://jodit.github.io/jodit-nodejs/dynamic-sources/

## Jodit Cloud: a file browser without a backend

The Jodit Cloud file browser runs the connector in that multi-tenant mode, with your API key as the tenant. If you use Jodit through Jodit Cloud, you no longer need to host a connector to get uploads working. Open your Cloud API key in the cabinet, switch to the new Storage tab, and enter the bucket, the prefix, its public URL and an access key pair. Press Test connection: the check lists the prefix, writes a small marker object and deletes it, so it exercises the three permissions the connector needs and tells you which one failed.

From then on the loader configures the editor for you. This is all the page needs:

```html
<script src="https://cloud.xdsoft.net/v4/jodit-pro/?key=YOUR_API_KEY"></script>
<textarea id="editor"></textarea>
<script>
    JoditLoader.ready().then(() => {
        Jodit.make('#editor');
    });
</script>
```

Uploads and the file browser go to `https://cloud.xdsoft.net/files/`, which resolves your bucket credentials by API key on every request, checks the page's domain against the key's allowed referrers, and talks to your bucket. Files never touch our storage, and we do not meter or limit that traffic.

### Who may use it

The API key is in your page source, so the Storage tab lets you choose how visitors are authorized.

In public mode every request from an allowed domain gets one fixed role. The default is Uploader: browse, upload, resize and crop images, but no renaming or deleting. Pick this when the page itself is behind your login.

In JWT mode your backend signs a short-lived HS256 token for the current user with a secret you set in the tab, and the `role` claim picks one of viewer, uploader, editor or admin. Requests without a valid token are refused.

```javascript
// Node.js backend: npm install jsonwebtoken
const jwt = require('jsonwebtoken');

const token = jwt.sign({ role: 'editor', sub: user.id }, process.env.JODIT_FILES_SECRET, {
    algorithm: 'HS256',
    expiresIn: '1h'
});
```

```html
<script src="https://cloud.xdsoft.net/v4/jodit-pro/?key=YOUR_API_KEY"></script>
<script>
    const token = '...'; // rendered into the page by your backend

    JoditLoader.ready().then(() => {
        Jodit.make('#editor', {
            filebrowser: { ajax: { headers: { Authorization: 'Bearer ' + token } } },
            uploader: { headers: { Authorization: 'Bearer ' + token } }
        });
    });
</script>
```

The roles map onto the connector's access control:

| Role | May |
| --- | --- |
| Viewer | browse and download |
| Uploader | Viewer plus upload, resize and crop images |
| Editor | Uploader plus rename, move, copy and delete files |
| Admin | Editor plus create and remove folders |

The cabinet guide covers the bucket policies, an AWS click-by-click setup and troubleshooting: https://xdsoft.net/jodit/pro/docs/cloud/file-storage.md

## Conclusion

Self-hosted: upgrade to `jodit-nodejs` 1.0.37 or later, set `storageAdapter: 's3'` on a source, and keep credentials in the environment. Multi-tenant setups get `resolveSources`. Jodit Cloud customers: connect a bucket in the Storage tab of the API key and the editor picks it up within five minutes, which is how long the loader is cached.

Further reading:

- Connector S3 guide: https://jodit.github.io/jodit-nodejs/aws-s3/
- Example on xdsoft.net: https://xdsoft.net/jodit/examples/intergration/jodit-nodejs-aws-s3.html
- Jodit Cloud file storage: https://xdsoft.net/jodit/pro/docs/cloud/file-storage.md

_Full page: https://xdsoft.net/blog/jodit-s3-file-storage_
