cd /news/ai-tools/show-hn-hycanvas-a-free-self-hostabl… · home topics ai-tools article
[ARTICLE · art-66495] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Show HN: HyCanvas, a free self-hostable Canva alternative

HyCanvas, a free self-hostable AI-native design platform, has been released as an open-source alternative to Canva. The platform supports social graphics, presentations, videos, whiteboards, docs, and print with no paywalls or watermarks, and uses a Go backend with a Next.js frontend, PostgreSQL, and optional S3-compatible storage.

read18 min views5 publishedJul 21, 2026
Show HN: HyCanvas, a free self-hostable Canva alternative
Image: source

HyCanvas is a free, self-hostable, AI-native design platform. Design anything - social graphics, presentations, videos, whiteboards, docs, and print - with no paywalls or watermarks. Web-only.

The product spans a single-player editor, document types (presentations, video, whiteboard, docs, sheets), export, brand kits, and a bring-your-own-key AI layer. Work not yet built is tracked under docs/roadmap/

.

Open a template and make it yours - edit text, restyle, and export, all in the browser:

Every design stays yours in an open format: download any document as a portable .hyc

file (readable JSON) and import it on any instance, or export to PNG, PDF, and more, with no watermarks:

The frontend and shared packages are an npm-workspaces monorepo (orchestrated with concurrently and dotenv-cli against a single shared root .env

); the backend is a standalone Go module.

backend

  • Go backend (REST under/api/v1

, the/realtime

WebSocket, the Go rendering engine for export, and DB migrations). Serves the exported frontend in the production bundle. Postgres only.frontend

  • Next.js app (Pages Router), statically exported for production.packages/*

  • framework-agnostic@hc/*

libraries (schema, engine, editor, sdk, color, text, geometry, export, media, stock, templates, authz, formula, sheets, timeline, whiteboard, docs, publishing, website, print, a11y, ...). The frontend imports them from their builtdist/

.scripts/build-dist.js

  • embeds the exported frontend into the Go binary (go build -tags embed

) and writes the single self-containeddist/hycanvas

.

  • illustrated user and operator guides:docs/

getting started,the dashboard,the editor,document types, and thefirst-run setup wizardwith every step screenshotted.docs/roadmap/

  • forward-looking specs for work not yet built (realtime collaboration, AI media, accessibility/i18n/enterprise).CLAUDE.md

  • working guidance for this repository.

  • Node 24 (see .nvmrc

) for the frontend and shared packages. - Go 1.25 for the backend ( backend

). - PostgreSQL. Object storage is optional (S3-compatible / MinIO); the backend falls back to local-file storage.

  • ffmpeg only if you want server-side video export (already bundled in the Docker image).
cp .env.example .env        # then edit values (at minimum DATABASE_URL + JWT_SECRET)
npm install                 # installs the frontend + shared packages
npm run build:packages      # build the @hc/* libraries once (the frontend imports their dist)
npm run db:migrate          # apply SQL migrations (Go migrator)
npm run dev                 # backend (Go) on :8005, frontend on :3000

Open http://localhost:3000

. The frontend reads the backend base URL from NEXT_PUBLIC_BACKEND_URL

(defaults to http://localhost:8005/api

).

Notes for development:

npm run build:packages

is required before the firstnpm run dev

(and after editing anypackages/*

source), because the@hc/*

packages are consumed from their compileddist/

, not their source.npm run build

andnpm run build:dist

build the packages for you.- The server also runs migrations on boot when DB_AUTO_MIGRATE=true

, sonpm run db:migrate

is mainly for an explicit, pre-boot migration. - No SMTP is wired: verify-email / password-reset / magic-link links are read from the dev outbox in non-production instead of being emailed.

The production bundle is a single self-contained binary: dist/hycanvas

. The statically-exported frontend is baked into it (go:embed

, built with -tags embed

), so one binary serves the frontend, the REST API, and the realtime WebSocket on one port, with no Node runtime and no sidecar public/

folder. It migrates the database on boot. The build also stamps the git version into the binary; it is logged on startup and returned by /healthz

and /api/v1/_go/health

.

npm run build:dist          # builds @hc/* + the frontend (routed to /api), embeds it, and compiles the Go binary into dist/
./dist/hycanvas       # run it directly: the binary loads .env itself, no Node needed

Running the bundle needs no Node at all: the Go binary loads a .env

from the working directory (or its parent) on startup, so ./dist/hycanvas

is fully standalone, exactly how it runs under Docker. Real environment variables always win over .env

, so injected config (containers, CI) is never overridden. npm run start:dist:only

is just a convenience alias for the same binary.

If you build the binary without the embedded UI (for example a plain npm run build:backend

), set PUBLIC_DIR

to an exported frontend directory to serve it; with neither, the binary serves the API only and shows a short notice page.

In the dist build the frontend talks to the same-origin /api

(no NEXT_PUBLIC_BACKEND_URL

needed), so the one process answers UI and API together.

The binary can also run itself as a background service, so no external process manager is needed. ./dist/hycanvas start

(or just ./dist/hycanvas

) runs in the foreground; the service

verbs manage a detached process with a pidfile and logfile next to the binary, on Linux, macOS, and Windows alike:

./dist/hycanvas service start       # detach into the background
./dist/hycanvas service status
./dist/hycanvas service log         # last log lines; -f follows
./dist/hycanvas service restart
./dist/hycanvas service stop

The binary's directory is the service's working directory, which is where .env

is read from. The service does not auto-start at boot; if you want that, add a crontab entry (@reboot /path/to/hycanvas service start

) or use Docker.

No .env

yet? Just start the server. On an interactive terminal it first asks whether to set up in the browser or right there in the terminal:

Web wizard(default): the server boots into setup mode and prints a one-time wizard access secret; opening any page redirects to/installation/step-1

, which asks for that secret and then walks through PostgreSQL, storage (local or S3), and optional SMTP, testing each answer live with visible progress. Step 1 includes "Running HyCanvas behind a proxy?": when enabled you configure the external domain (e.g.https://hycanvas.art

) separately from the internal host and port the proxy forwards to (written asAPP_URL

,BIND_HOST

, andPORT

). Answers are held on the server (never in the browser), and a page refresh always restarts at the welcome step.CLI wizard: the same questions asked in the terminal, with the same live validation and hidden password input.

Either way the wizard writes .env

(secrets like JWT_SECRET

are generated automatically), runs the database migrations, starts the app in the same process, and (in the browser flow) creates your first account. Non-interactive starts (Docker, pipes) default to the web wizard. To skip all of it, create a .env

by hand (see .env.example

) before starting.

npm run deploy

rebuilds the bundle and restarts the running service in one step (build:dist

  • ./dist/hycanvas service restart

).

When writing .env

by hand for production, set at minimum NODE_ENV=production

, DATABASE_URL

, a strong JWT_SECRET

, APP_URL

(public base URL used in generated links), and an absolute LOCAL_STORAGE_PATH

(or STORAGE_DRIVER=s3

with S3_*

). AI provider keys are configured per workspace at runtime (stored encrypted), never via env.

HyCanvas serves plain HTTP; put nginx, Caddy, or Traefik in front for TLS. Three settings matter: APP_URL

is the external domain the proxy serves (used in generated links and the OIDC redirect), PORT

is the internal port the proxy forwards to, and BIND_HOST=127.0.0.1

keeps the app reachable only through the proxy. The setup wizard configures all three when you answer "Running HyCanvas behind a proxy?" in step 1. The proxy must forward the Host

header and (for realtime collaboration) WebSocket upgrades on /realtime

. With an https APP_URL

, session cookies stay Secure

automatically.

Also raise the proxy's request-body limit: uploads travel as base64 JSON, about a third larger than the file on disk, and nginx's default client_max_body_size

of 1 MB rejects anything bigger with a 413 before HyCanvas ever sees it. Set it to comfortably above the largest upload you expect, for example client_max_body_size 100m;

. Caddy and Traefik impose no body limit by default.

Social sign-in is configured via env and appears on the login and signup pages once set. For Google:

  • In Google Cloud Console, configure the OAuth consent screen, then create an OAuth client ID of type "Web application". - Add the authorized redirect URI: <APP_URL>/api/v1/auth/oidc/callback

(for examplehttps://hycanvas.art/api/v1/auth/oidc/callback

). - Set in .env

and restart (./dist/hycanvas service restart

):

OIDC_ISSUER=https://accounts.google.com
OIDC_CLIENT_ID=<client id>.apps.googleusercontent.com
OIDC_CLIENT_SECRET=<client secret>
OIDC_LABEL=Google

A "Continue with Google" button then shows on the auth pages. Any standards-compliant OIDC provider works the same way via its issuer URL; OIDC_ALLOWED_EMAIL_DOMAINS

restricts sign-in to listed domains, and OIDC_REDIRECT_URI

overrides the callback when the default does not fit. The issuer must be https (localhost excepted), and APP_URL

must be set correctly, especially behind a proxy.

Three sign-in methods ship (email + password, magic link, and OIDC/SSO), and each has an independent login and signup toggle so you can decide exactly how people get in and how accounts are created. Set any of these to true

or false

; anything else keeps the default. The defaults leave the product as it was before these settings existed, so you only set what you want to change.

Variable Default Effect
AUTH_PASSWORD_LOGIN_ENABLED
true
Sign in with email + password
AUTH_PASSWORD_SIGNUP_ENABLED
true
Create an account with a password
AUTH_MAGICLINK_LOGIN_ENABLED
true
Emailed sign-in link for an existing account
AUTH_MAGICLINK_SIGNUP_ENABLED
false
Emailed link that creates a new account (create-on-click; nothing is created until the link is opened)
AUTH_OIDC_LOGIN_ENABLED
true
SSO sign-in (needs the OIDC_* settings above)
AUTH_OIDC_SIGNUP_ENABLED
true
SSO creates a new account on first login

The sign-in page shows only the methods you leave on, so no user sees a form they cannot use. The toggles are enforced on the server too: a disabled method's endpoint returns 403, not just a hidden button.

Two guardrails apply automatically. The OIDC toggles do nothing unless OIDC is configured. And if a combination would leave no usable way to sign in, the server keeps password login on and logs a warning, so a typo cannot lock you out.

To run an instance where every account comes from your identity provider (no passwords, no magic links), turn the local methods off and leave OIDC on:

AUTH_PASSWORD_LOGIN_ENABLED=false
AUTH_PASSWORD_SIGNUP_ENABLED=false
AUTH_MAGICLINK_LOGIN_ENABLED=false

Note when switching an existing instance to SSO-only: a user who already has a password account links their SSO identity to it automatically only when the provider's email domain is listed in OIDC_ALLOWED_EMAIL_DOMAINS

(you are vouching that the provider owns those emails). Without that, their first SSO login creates a separate account. Set the allowlist before flipping the switch if you have existing password users.

Protect login, signup, password-reset request, and magic-link request from automated abuse with Cloudflare Turnstile or Google reCAPTCHA. It is off until you set a provider:

CAPTCHA_PROVIDER=turnstile          # or "recaptcha"
CAPTCHA_SITE_KEY=<public site key>  # from the provider dashboard
CAPTCHA_SECRET_KEY=<secret key>

The site key is sent to the browser (public by design) and the sign-in page renders the widget; the secret verifies each response server-side. The redeem endpoints (password-reset and magic-link, which carry a token from an email, not a form) are not gated.

Two behaviors to know. It is fail-closed: if the provider is unreachable, the gated forms reject, so a solved challenge is always required once enabled; a provider outage blocks these forms, which is the correct default for a security control but is worth planning for. And a provider set without both keys is treated as disabled (and logged), so a half-configuration never 403s every login. Both instances behind Cloudflare make Turnstile the natural choice; reCAPTCHA works the same way via its keys.

Started on local-disk storage and want object storage later? The binary migrates itself:

./dist/hycanvas service stop       # stop first so no new objects land mid-copy
./dist/hycanvas storage migrate    # copies every object, verifies, updates .env
./dist/hycanvas service start

The S3 target (AWS, MinIO, R2, ...) is taken from S3_*

in the environment when present, or asked interactively with a connectivity check; --dry-run

previews the object count and size, and --yes

makes it non-interactive for scripts. The copy is idempotent (already-present objects are skipped, so re-runs are safe), the database needs no changes (it stores storage keys, not URLs), and the local files are kept as a rollback until you delete them.

Releases on the GitHub releases page ship the same self-contained binary prebuilt for Linux (amd64, arm64), macOS (Intel, Apple Silicon), and Windows (amd64). Each archive contains just the hycanvas

binary (the first-run wizard generates the configuration), and a SHA256SUMS.txt

accompanies the archives for verification. You still need PostgreSQL; ffmpeg is only required for server-side video export.

tar -xzf hycanvas_<version>_<os>_<arch>.tar.gz && cd <unpacked dir>
./hycanvas service start    # asks: browser wizard or terminal wizard

The macOS binaries are not signed or notarized; if macOS quarantines the download, run xattr -d com.apple.quarantine hycanvas

or right-click the binary and choose Open.

The whole product (UI + REST API + realtime WebSocket) runs from one image, with Postgres as a companion service. For the full environment-variable reference and production options, see DOCKER_SETUP.md.

The repo ships a docker-compose.yml

that pulls the prebuilt image (linux/amd64

and linux/arm64

) and runs it against your own managed Postgres (it ships no bundled db

):

cp .env.example .env   # set JWT_SECRET, APP_PORT, and a reachable DATABASE_URL
docker compose up -d

Then open http://localhost:<APP_PORT>

(e.g. http://localhost:8005

). JWT_SECRET

, APP_PORT

, and a reachable DATABASE_URL

(or EXTERNAL_DATABASE_URL

) are required and migrations run on boot; inside a container localhost

is the container, so use host.docker.internal

for a DB on your machine (mapped via extra_hosts

) or a managed endpoint. Update later with docker compose pull && docker compose up -d

. If you'd rather have Postgres bundled for a quick trial, the self-contained compose in docker/README.md includes a db

service and runs anywhere.

To run your own source instead of the published image, use the build variant (it builds Dockerfile

and adds a container healthcheck + restart policy):

docker compose -f docker-compose.prod.yml up --build -d

The build variant serves at http://localhost:8005

; the published docker-compose.yml

publishes on the host port you set in APP_PORT

. Stop with docker compose down

. The build variant can run a bundled Postgres (COMPOSE_PROFILES=bundled

); add -v

to drop that Postgres volume.

Notes:

  • ffmpeg (for video export) is bundled in the image; no host install needed.
  • Local-file storage persists on the host at ./data/storage

(bind-mounted to/app/.data/storage

) with the publisheddocker-compose.yml

, so it survivesdocker compose down -v

. To use object storage instead, set theS3_*

variables on theapp

service. - Secrets/config (notably JWT_SECRET

, and optionallyAI_SECRET

,OIDC_*

,VAPID_*

) come from your.env

. - Database: the published docker-compose.yml

always uses an external Postgres, setDATABASE_URL

orEXTERNAL_DATABASE_URL

in.env

. The build-from-sourcedocker-compose.prod.yml

can bundle Postgres withCOMPOSE_PROFILES=bundled

, or point it external by emptyingCOMPOSE_PROFILES

and settingEXTERNAL_DATABASE_URL

. For Postgres on the host, usehost.docker.internal

(both compose files map it viaextra_hosts

).

To work on the code in containers, with no local Go or Node toolchain, use the dev stack. It runs the Go backend on :8005

and the Next.js dev server on :3000

with live reload against your working tree:

docker compose -f docker-compose.dev.yml up --build

Open http://localhost:3000

. See DOCKER_SETUP.md for details.

All configuration is read from the root .env

(copy .env.example

). The most important keys:

Variable Mode Description
DATABASE_URL
both Postgres connection string. Required.
JWT_SECRET
both Signs session tokens (also the fallback key for encrypting AI keys). Use a strong value in production.
PORT
both Backend port (default 8005 ).
NODE_ENV
both development or production .
DB_AUTO_MIGRATE
both When true , the server applies migrations on boot.
NEXT_PUBLIC_BACKEND_URL
dev Frontend API base in dev (http://localhost:8005/api ). Unused in the dist build (it calls same-origin /api ).
FRONTEND_URL
both Allowed CORS origin for the API (dev: http://localhost:3000 ).
APP_URL
prod Public base URL used in generated links (verify-email, magic-link, share).
STORAGE_DRIVER
both local (default) or s3 .
LOCAL_STORAGE_PATH
both Absolute path for local-file storage when STORAGE_DRIVER=local . Must be absolute so dev and the dist bundle share it.
S3_*
optional S3-compatible object storage (endpoint, bucket, keys) when STORAGE_DRIVER=s3 .
ASSET_QUOTA_BYTES
optional Per-workspace upload cap in bytes (default 2 GiB).
USER_STORAGE_QUOTA_BYTES
optional Global per-user upload cap across all workspaces; unset/0 = unlimited. For public instances.
AI_SECRET
optional Encrypts stored per-workspace AI keys; falls back to JWT_SECRET .
OIDC_*
optional OIDC single sign-on (issuer, client id/secret).
AUTH_*_ENABLED
optional Enable/disable each sign-in method's login and signup (password, magic link, OIDC). Defaults preserve prior behavior. See

CAPTCHA_*

CAPTCHA_PROVIDER

, CAPTCHA_SITE_KEY

, CAPTCHA_SECRET_KEY

, CAPTCHA_MIN_SCORE

). Off unless a provider is set. See CAPTCHA on the auth forms.GOOGLE_ANALYTICS_ID

G-XXXXXXXXXX

). When set, the server injects the gtag.js snippet into served HTML pages at runtime; empty means no analytics.VAPID_*

POSTGRES_USER

/ POSTGRES_PASSWORD

/ POSTGRES_DB

/ COMPOSE_PROFILES

docker-compose.yml

.| Command | Description | |---|---| npm run dev | Run the Go backend and the frontend together with hot reload. | npm run build:packages | Build the @hc/* libraries (needed before dev on a fresh checkout). | npm run build | Build packages, the Go binary, and the frontend. | npm run gen:theme | Regenerate color tokens from frontend/src/theme.config.mjs (the frontend build runs this automatically). | npm run db:migrate | Apply SQL migrations (Go migrator). The server also migrates on boot. | npm run lint | Vet the Go backend and lint the frontend. | npm run test | Run the package and Go backend tests. | npm run build:dist | Compile the single dist/hycanvas binary with the frontend embedded (-tags embed ) and the git version stamped in. | npm run start:dist:only | Run the already-built binary (alias for ./dist/hycanvas , which loads .env itself). | npm run deploy | Build the dist bundle and restart the built-in service (hycanvas service restart ). |

The starter template catalog lives in backend/internal/templates/seed.json

and is compiled into the binary (//go:embed

). There is no seed command or database step: the templates are served directly from the embedded JSON, merged with any templates saved to the database.

To add or edit a built-in template, edit seed.json

, then rebuild/restart the backend so it re-embeds the file:

  • Development: restart npm run dev

(it runs the backend viago run

, which recompiles each start). - Production (native): npm run build:dist

thennpm run start:dist:only

(ornpm run deploy

). - Docker: rebuild from source with docker compose -f docker-compose.prod.yml up --build

(the defaultdocker compose up

pulls the published image and won't include local edits).

User-saved templates (Save as template) are stored in the database and need no rebuild.

The app's color identity lives in one file: frontend/src/theme.config.mjs

(the brand and accent scales, the identity gradient, the editor canvas-overlay colors, and the collaborator presence palette). To rebrand, edit that file and run npm run gen:theme

. The generator rewrites the Tailwind CSS tokens, the typed canvas-overlay constants, and the Go presence palette, so one change propagates across the UI chrome, the gradient, the logo, the favicon/theme-color, the canvas overlays, and presence colors. npm run gen:theme:check

(run as part of npm run lint

) fails if the committed generated files drift from the source.

This product/app accent is intentionally separate from the per-workspace Brand Kit, which themes design content rather than the app shell.

Two long-lived branches: development

(ongoing work) and stable

(release-ready). Pushing commits builds no Docker images and cuts no releases; CI runs tests only. Releasing is explicit:

  • Merge development

intostable

when it is release-ready. - Tag on stable

:git tag v0.2.0 && git push origin v0.2.0

. The release workflow REFUSES tags whose commit is not onstable

.

One tag then drives everything (.github/workflows/release.yml

):

Binary release: the frontend builds once, the embedded binary cross-compiles for all five platforms (pure Go,CGO_ENABLED=0

), and a GitHub Release is published with the archives andSHA256SUMS.txt

. Hyphenated tags (v0.2.0-rc.1

) are marked pre-release.Docker images, always lean (thedocker

job packages the just-built release binaries into the runtime-onlyDockerfile.release

; no toolchains, no from-source image builds anywhere):- a final tag v0.2.0

publisheshycanvas/hycanvas:latest

,:development

,:0.2.0

, and the rolling minor alias:0.2

; - a pre-release tag v0.2.0-rc.1

publishes:development

and:0.2.0-rc.1

only, so the development channel previews the next release whilelatest

stays on the last final one.

  • a final tag Docker Hub overview: any change todocker/README.md

ondevelopment

is synced to the Docker Hub repository description automatically by.github/workflows/dockerhub-description.yml

(also runnable manually from the Actions tab).

The Actions "Run workflow" button on the release workflow does a build-only dry run without releasing or publishing images.

The version stamped into the binary (git describe

) is logged on startup and reported by /healthz

.

  • TypeScript everywhere, strict mode. Keep the rendering engine free of any React or UI dependency.
  • For shipped features, match the surrounding code; for unbuilt work, read the relevant docs/roadmap/

spec first. - Never use longdash characters or standalone three-hyphen horizontal rules in markdown.

See CONTRIBUTING.md

for setup and the pull-request checklist, CODE_OF_CONDUCT.md

for community expectations, and SECURITY.md

for reporting vulnerabilities.

HyCanvas is licensed under the Elastic License 2.0 (see LICENSE

). This is a source-available license: you may use, modify, self-host, and redistribute it freely, but you may not provide it to third parties as a hosted or managed service, circumvent the license-key functionality, or remove the licensing notices. Third-party and bundled-asset notices are in NOTICE

.

Hosting HyCanvas for your own customers? That needs acommercial licensefrom HyScaler; internal use and self-hosting never do.Contributing? All contributions require a one-timeContributor License Agreement, signed with a single comment on your first pull request.

© 2026 HyScaler. HyCanvas is a HyScaler product. HyScaler® is a registered trademark.

── more in #ai-tools 4 stories · sorted by recency
── more on @hycanvas 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-hycanvas-a-f…] indexed:0 read:18min 2026-07-21 ·