# Embed Generator: building Discord embeds, button roles and scheduled messages visually

> Embed Generator is a free web editor for Discord messages, hosted at message.style and open source under MIT. It builds embeds, button roles, scheduled messages and Components V2 layouts, and can send them through a webhook or its own bot, with a Go backend that shards across instances.

**merlinfuchs/embed-generator** — Free visual editor and bot for Discord messages: embeds, button roles, scheduled messages and Components V2, sent through webhooks or the bot. Hosted at https://message.style

- Repository: https://github.com/merlinfuchs/embed-generator
- Website: https://message.style
- Stars: 717 · Forks: 59
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/merlinfuchs-embed-generator

## A message builder for embeds, button roles and scheduled sends

Embed Generator is a free visual editor for Discord messages, hosted at message.style and open source under the MIT licence. The scope covers four things that normally take a Discord developer a JSON payload and a hopeful bot token: embeds, button roles, scheduled messages and Components V2 layouts. A finished message is sent either through a plain Discord webhook or through the Embed Generator bot, so the same editor covers both a one-off announcement and a bot driven interaction.

The audience is server operators and community managers rather than application developers. Someone maintaining a Discord community wants an embed with a title, description, fields, colours and a footer, and they want button roles where a click on a button grants a role, and they want a message to appear at nine in the evening without anybody being awake to post it. Doing that by hand means constructing embed objects and calling the webhook endpoint, which is fine once and tedious repeatedly. The repository's topic tags include discohook-alternative, which places the project in the same conversation as the well known embed builder, and the project page links a YouTube tutorial for people who would rather watch the editor in use than read about it.

One instruction deserves attention before anything else: self hosting works, but there is not much benefit to it. That is the author telling you that the hosted version is the intended path, and the self hosting section exists for people who need control, not for people who want the free tier of a product.

## One Go binary carrying the API, the app and the site

The build layout explains the shape of the project. There is a Go backend in embedg-server, a frontend in embedg-app, a marketing site and documentation in embedg-site, and a go.work file tying the Go side together. The Dockerfile installs Node and pnpm, builds the site, builds the app, then compiles the backend with the embedapp and embedsite build tags so the frontend output is embedded in the binary, and the final image is a debian slim layer with a single executable. The container exposes port 8080 and starts with a migration step followed by the server:

```dockerfile
EXPOSE 8080
CMD ./embedg-server migrate postgres up; ./embedg-server server
```

So the deployment surface is one binary, one Postgres and one S3 compatible store. The S3 credentials are required because image uploads go there, and the compose file runs MinIO to provide that. Data that is not an image, the users, the templates, the guild and role mappings that make button roles work, lives in Postgres. The two dependency choices are conventional and the operational cost of both is well understood, which is a point in the project's favour compared with a bespoke storage layer.

There is a wrinkle worth knowing. The Dockerfile is a multi stage build that pulls Node from the NodeSource apt repository and prepares a specific pnpm version with Corepack, so building the image is a heavier operation than a typical Go build and needs network access to two distribution sources.

## Bringing up the stack with the compose file in the repository root

The quickest path to a running instance is the compose file that ships with the project. It starts Postgres with a named volume and a pg_isready healthcheck, MinIO for image storage, and the embedg service itself, which waits for Postgres to report healthy before it starts. The published port is 8080 on the host, and the environment block points the server at the Postgres container and at the MinIO endpoint. The embedg service in that file looks like this:

```yaml
  embedg:
    image: merlintor/embed-generator:latest
    ports:
      - "8080:8080"
    environment:
      - EMBEDG_API__HOST=0.0.0.0
      - EMBEDG_API__INSECURE_COOKIES=true
      - EMBEDG_DATABASE__POSTGRES__HOST=postgres
      - EMBEDG_DATABASE__S3__ENDPOINT=minio:9000
```

You should not configure Postgres in your own config file, because the compose setup injects that instance, and the file also mounts your local embedg.toml into the container at /root/embedg.toml, which is how your Discord credentials reach the server. Two of those settings are worth understanding rather than copying blind. EMBEDG_API__INSECURE_COOKIES=true tells the API to accept cookies without the secure flag, which is correct for plain HTTP on localhost and wrong for a public deployment. The public URL variables point the API and the app at http://localhost:8080, so a hosted instance needs those rewritten to real origins.

## Sharding across instances with instance_count and instance_index

Discord distributes a bot's guilds into shards, and Embed Generator lets several server instances cooperate. The mechanism is a slice assignment rather than a leader election. Every instance is given the same shard_count, and each one declares how many instances exist and which index it is, so instance i of n handles every n-th shard:

```toml
[discord]
shard_count = 250
instance_count = 5
instance_index = 0   # 1, 2, 3, 4 on the others; EMBEDG_DISCORD__INSTANCE_INDEX works too
```

The documentation explains that together the instances cover every shard exactly once even when the count does not divide evenly, which is the part that makes this useful for a real bot. Instance 0 holds shard 0 and also runs the work that happens once per deployment rather than once per guild, so scheduled message dispatch and similar jobs are not duplicated across the fleet. The older shard_ids field is still there for an irregular split, and the project points at the instance based approach as the one to use.

The implication for an operator is that adding a second instance is a configuration change and a redeploy, not a migration, but getting it wrong means either duplicated or missing guilds. A shard count of 250 with five instances is the example given, and the Discord token has to be the same for all of them.

## embedg.toml, --config and the EMBEDG_ environment variables

Configuration has three entry points and they resolve in a predictable order. The simplest is to copy embedg.example.toml to embedg.toml next to the binary and fill in the Discord credentials. If you want the file somewhere else, pass --config with a path, for example embedg-server server --config /etc/embedg/embedg.toml, or set the EMBEDG_CONFIG environment variable. The same settings can also be expressed as environment variables, with a doubled separator between the sections, so EMBEDG_DISCORD__TOKEN sets the Discord token and EMBEDG_DATABASE__POSTGRES__HOST sets the Postgres host. That is the mechanism the compose file relies on for its whole environment block.

The path config and the environment config compose by section, which means you can keep a checked in embedg.toml for the Discord section and let the environment supply the database and S3 details. The development instructions make the same point in a different way: docker-compose.dev.yaml starts Postgres and MinIO and nothing else, and the credentials match the defaults in embedg-server/config/default.toml, so a local embedg.toml only needs the Discord section.

For anyone upgrading, there is a specific wrinkle. Configuration in the old config.yaml format still loads for versions up to v0.6, but it is deprecated, and MIGRATION.md is the file to read for the conversion and the other changes. That note is short, so a reader with an old config should treat MIGRATION.md as required reading rather than optional.

## Where self hosting stops being worth it

The limitations are structural, and the project is unusually candid about the first one. Self hosting an Embed Generator instance means running Postgres, running an S3 compatible store, keeping a Go binary up to date, and managing a Discord bot connection across however many shards you serve. The author says there is not much benefit to it over the hosted version at message.style. If your reason to self host is cost, the hosted service is free, so the comparison collapses. If your reason is control over data, over a private network, or over scheduling, then the work above is the price.

The second limitation is that the compose file ships credentials that are fine on a laptop and wrong on a server. The MinIO root password is a literal in the file, EMBEDG_API__INSECURE_COOKIES=true is set, and the Postgres container uses POSTGRES_HOST_AUTH_METHOD: trust, which means no password is required to connect. None of that is acceptable on a shared host, and turning it off is your responsibility rather than the file's.

The third is scope. The documentation covers the editor, the webhook and bot sending paths, sharding, storage and configuration. It does not cover a backup or restore procedure for Postgres, a rollback path for a failed migration, a documented upgrade procedure for the binary, or any access control model for the web editor itself. The container starts by running the migration command, so the schema is expected to move forward with the binary. For a community tool that is fine. For a scheduler that dispatches messages on behalf of a business, the absence of a documented restore story is the thing to solve first.

## Building the app, the site and the server from source

There is a source build for contributors, and the three pieces are built independently. The frontend lives in embedg-app and uses pnpm, enabled through Corepack which ships with Node:

```sh
cd embedg-app
corepack enable
pnpm install
pnpm dev
pnpm build
```

The home page and documentation live in embedg-site and follow the same pattern, with pnpm start for the development server instead of pnpm dev. Both expect Node and npm from nodejs.org. The backend is Go, and Go 1.25 or newer is asked for:

```sh
cd embedg-server
go run main.go migrate postgres up
go run --tags "embedapp embedsite" main.go server
go build -o embedg-server --tags "embedapp embedsite"
```

The build tags decide whether the compiled binary carries the frontend files. With embedapp and embedsite included you get one self contained binary, and without them you have to serve the frontend yourself. The development database comes up separately, and MinIO creates its buckets on startup:

```sh
docker compose -f docker-compose.dev.yaml up -d
```

For a first run the sensible order is the compose stack, since it gives you Postgres and MinIO with matching defaults, then the migrations, then the server.

## MIT licence and a release cadence measured in days

The licence is MIT, which is about as permissive as software licences get, and the repository carries an MIT LICENSE file. For a tool you might fork, rebrand or embed in an internal community setup, the practical constraints are the obligation to keep the copyright and licence notice and the disclaimer of warranty. There is no copyleft reach into your own code.

Activity is high and recent. The last push was on 2026-10-16, and the release list shows v0.8.0 on 2026-09-30, v0.9.0 on 2026-10-07 and v0.10.0 on 2026-10-11. The versions are still in the zero point range and the increments are fast, so a release can carry behaviour changes you did not ask for. Prebuilt binaries are also published on the releases page with the backend and frontend bundled into a single file, which is the shortest path to a self hosted instance and avoids the multi stage Docker build entirely.

Two operational notes close this out. The container image is published as merlintor/embed-generator, and the compose file tracks the latest tag, so a pull can change the version under you between restarts. And because the container runs migrations on start, a newer image against an older database is the expected path rather than an error, which makes pinning the image tag a reasonable precaution.

## Conclusion

Embed Generator fits server operators and community managers who want a visual Discord message builder instead of hand written JSON, and who can accept a hosted service or the work of running Postgres, MinIO and a Go binary themselves. It does not fit teams that need audited access control, that must keep message templates in their own repository with review, or that expect a self hosted instance to be simpler than the hosted one. Before adopting, read MIGRATION.md if you are coming from v0.6 or older, then confirm that the Discord token and S3 credentials are set, that instance_count and instance_index are correct for your shard plan, and that scheduled messages are allowed under Discord's own rules. The last push was on 2026-10-16 and the current release is v0.10.0, so the project is moving quickly and pinning a version is reasonable.

## FAQ

### What is Embed Generator and what can it build?

Embed Generator is a free visual editor for Discord messages, available hosted at message.style and open source under the MIT licence. It builds embeds, button roles, scheduled messages and Components V2 layouts, and sends them through a Discord webhook or the Embed Generator bot.

### How do I self host Embed Generator with Docker?

Use the docker-compose.yaml in the repository root, which starts Postgres, MinIO and the embedg image, then run docker-compose up. The editor is then available at http://localhost:8080, and the compose file mounts your embedg.toml into the container for the Discord credentials.

### How do I run several Embed Generator instances across Discord shards?

Give every instance the same shard_count, and set instance_count and instance_index so each instance takes every n-th shard, with instance 0 also handling work that runs once per deployment. The instances then cover every shard exactly once even when the count does not divide evenly.

### What do I need to configure before Embed Generator will start?

Copy embedg.example.toml to embedg.toml next to the binary and fill in the Discord credentials, or set the same values as environment variables with a doubled separator, such as EMBEDG_DISCORD__TOKEN. The S3 credentials are required as well, because image uploads are stored there, and a Postgres database is needed for the rest.

### What changed in Embed Generator for users upgrading from v0.6 or older?

An existing config.yaml still loads but is deprecated, and MIGRATION.md describes how to convert it along with the other changes in that upgrade.

## Sources

- [License: MIT](https://github.com/merlinfuchs/embed-generator/blob/main/LICENSE)
- [merlinfuchs/embed-generator on GitHub](https://github.com/merlinfuchs/embed-generator)
- [Project website](https://message.style)
- [README](https://github.com/merlinfuchs/embed-generator/blob/main/README.md)
- [Releases](https://github.com/merlinfuchs/embed-generator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/merlinfuchs-embed-generator
