Free prelaunch hosting
Vitni’s prelaunch deployment is a $0 demonstration environment. It is not a production availability commitment. The providers can sleep, rate-limit, pause, or change free-tier limits. No Certo users, credentials, signing keys, or customer records are migrated.
Service layout
Section titled “Service layout”| Hostname | Service | Provider |
|---|---|---|
certificate.vitni.cc | Marketing site, workspace, badge verification | Cloudflare Workers Free |
docs.certificate.vitni.cc | Astro/Starlight documentation | Cloudflare Workers Free static assets |
api.certificate.vitni.cc | NestJS API and OpenAPI | Render Free web service |
| OIDC issuer URL | Hosted login and identity | ZITADEL Cloud Free generated domain |
| Scheduled email delivery | Outbox processor | Cloudflare Workers Free + Hyperdrive + Resend Free |
| PostgreSQL | Application data | Neon Free, EU region |
The diagram and architecture decisions are recorded in the deployment design.
Create the Neon database
Section titled “Create the Neon database”-
Create a Neon Free project in an EU region. Use its default database,
neondb. -
Create service roles through the Neon SQL Editor, not the Neon Console, API, or CLI. Neon grants Console-created roles membership in
neon_superuser, which bypasses RLS and carries database/role administration privileges. The SQL-created roles below stay separate from that administrator role. See Neon’s role guidance.CREATE ROLE vitni_migrator_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT;CREATE ROLE vitni_app_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT;CREATE ROLE vitni_worker_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOINHERIT;GRANT CONNECT ON DATABASE neondb TO vitni_migrator_runtime, vitni_app_runtime, vitni_worker_runtime;GRANT CREATE ON DATABASE neondb TO vitni_migrator_runtime;GRANT USAGE, CREATE ON SCHEMA public TO vitni_migrator_runtime;GRANT USAGE ON SCHEMA public TO vitni_app_runtime, vitni_worker_runtime;ALTER DEFAULT PRIVILEGES FOR ROLE vitni_migrator_runtime IN SCHEMA publicGRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO vitni_app_runtime;ALTER DEFAULT PRIVILEGES FOR ROLE vitni_migrator_runtime IN SCHEMA publicGRANT USAGE, SELECT ON SEQUENCES TO vitni_app_runtime;For an existing Vitni database whose objects are still owned by the old Console-created
vitni_migrator, grant that role temporary membership before applying migration0021_runtime_role_tightening:GRANT vitni_migrator_runtime TO vitni_migrator;After the migration transfers object ownership to
vitni_migrator_runtime, remove the temporary membership:REVOKE vitni_migrator_runtime FROM vitni_migrator;Do not put passwords in SQL Editor history. After creating the SQL roles, use Neon Console’s password-reset action for each role and set the passwords yourself, then copy each connection URL. Keep the existing Neon Console roles (
neondb_owner,vitni_app,vitni_worker, andvitni_migrator) out of Render and Cloudflare. -
Copy
.env.production.exampleto the ignored.env.production.local. Put the three SQL-role URLs there, using Neon TLS and the non-pooled connection string for migrations. Keep this file private. -
Run the fresh-schema migrations once:
Terminal window corepack pnpm db:migrate:production
Use the pooled vitni_app_runtime URL for Render’s API if needed. For Hyperdrive, use the direct vitni_worker_runtime URL: Hyperdrive supplies the connection pool. Never put an owner or Console-created Neon role URL in Render or Cloudflare.
Configure ZITADEL OIDC
Section titled “Configure ZITADEL OIDC”- Use the configured ZITADEL Cloud instance
https://vitni-r5stjz.eu1.zitadel.cloudand Vitni project397023330298777565. - The web app
Vitni Webuses client ID397023387190330854, Authorization Code with PKCE (S256), and no client secret. - Add the exact redirect URL
https://certificate.vitni.cc/auth/callbackand post-logout URLhttps://certificate.vitni.cc/. - Enable refresh tokens, set the web application’s access token type to JWT, and enable user information inside the ID token for the web session.
- Set the token audience to the Vitni project. Its project ID is
397023330298777565; the matching audience scope isurn:zitadel:iam:org:project:id:397023330298777565:aud. - The generated ZITADEL domain is used for login; custom login domains require a paid ZITADEL plan.
Set these values in Render and the web Worker’s public variables:
VITNI_OIDC_ISSUER=https://vitni-r5stjz.eu1.zitadel.cloudVITNI_OIDC_AUDIENCE=397023330298777565NUXT_OAUTH_ISSUER=https://vitni-r5stjz.eu1.zitadel.cloudNUXT_OAUTH_CLIENT_ID=397023387190330854NUXT_OAUTH_REDIRECT_URL=https://certificate.vitni.cc/auth/callbackNUXT_OAUTH_POST_LOGOUT_REDIRECT_URL=https://certificate.vitni.cc/NUXT_OAUTH_SCOPE=openid profile email offline_accessNUXT_OAUTH_API_AUDIENCE_SCOPE=urn:zitadel:iam:org:project:id:397023330298777565:audThe API validates the ZITADEL JWT access token through discovered JWKS, then uses the discovered UserInfo endpoint to obtain verified email claims. This is needed because ZITADEL keeps email claims out of JWT access tokens. Vitni’s API still enforces its own organization membership and role checks; ZITADEL organizations do not replace Vitni’s tenant authorization model. For local development, .env keeps the local Keycloak issuer; self-hosters can set any compatible OIDC issuer, client ID, and audience.
Configure Render
Section titled “Configure Render”The root render.yaml defines one Frankfurt Docker web service using infra/render/api.Dockerfile, the /api/v1/health readiness check, and the api.certificate.vitni.cc custom domain. The separate GitHub repository is private while source reuse and licensing are reviewed. Connect it in Render and apply the Blueprint after the repository access is approved.
Set these secret environment values in Render:
VITNI_DATABASE_URL: thevitni_app_runtimeSQL-role connection URL.VITNI_SIGNING_KEY_ENCRYPTION_KEY: a newly generated 32-byte key encoded as 64 hexadecimal characters. Keep a secure recovery copy; never reuse a Certo key.VITNI_OIDC_ISSUERandVITNI_OIDC_AUDIENCE: the ZITADEL issuer and Vitni backend project ID.
The service binds to Render’s PORT and 0.0.0.0. Add the DNS record Render shows for its custom domain in Cloudflare. Leave it DNS-only while Render provisions TLS; proxy it only after the domain is verified.
Configure the Cloudflare Workers
Section titled “Configure the Cloudflare Workers”Wrangler deploys only the web app, docs, and scheduled email Worker. Cloudflare Containers are removed from the deployment path.
-
Set
NUXT_OAUTH_ISSUER,NUXT_OAUTH_CLIENT_ID, the redirect URLs, scope, and audience scope inapps/web/wrangler.jsoncbefore deploying the site. -
Create a fresh
NUXT_SESSION_PASSWORDwith at least 32 random characters and set it as a secret onapps/web/wrangler.jsonc. -
Create a Hyperdrive configuration in Cloudflare pointing to the Neon
vitni_worker_runtimeSQL role. Add its ID as theHYPERDRIVEbinding ininfra/cloudflare/email-worker/wrangler.jsonc. Store only the worker role’s connection in Hyperdrive; do not put it in Worker vars. -
Verify a sender domain in Resend. Add its SPF/DKIM records in Cloudflare, set the
VITNI_EMAIL_FROMWorker variable, and putRESEND_API_KEYand the same new signing-key encryption secret into the email Worker’s secret store. -
Keep
VITNI_EMAIL_DELIVERY_ENABLEDset tofalseuntil the sender, Hyperdrive binding, and secrets are ready. The Worker claims at most five queued messages per five-minute run and sends with a per-outbox idempotency key. -
Deploy the Worker assets:
Terminal window corepack pnpm deploy:cloudflare
The email Worker uses the existing encrypted email_outbox format. Its Node/SMTP runner remains available for local and self-hosted use; Cloudflare uses the Resend HTTPS API because Workers and free Render services do not support outbound SMTP delivery.
Free-tier behavior
Section titled “Free-tier behavior”- Cloudflare Workers Free currently permits 100,000 requests per day, 10 ms CPU per invocation, and five Cron Triggers per account. Hyperdrive has its own free-tier query allowance. Check the account’s existing Cron Trigger use before adding the schedule.
- Render Free web services sleep after 15 idle minutes and can take about a minute to wake. They share 750 free instance hours per workspace each month. Render says Free is for testing and hobby use, not production.
- Neon Free currently provides 1 GB per project and 100 compute-hours per month; compute scales to zero.
- ZITADEL Cloud Free currently allows 100 daily active users on one instance and does not include a custom domain or availability SLA.
- Resend Free currently includes 3,000 emails per month and a 100-per-day cap.
Do not add keep-alive traffic or paid overages. If the prelaunch limits become insufficient, stop and review a paid production architecture before upgrading any service.
Verify the deployment
Section titled “Verify the deployment”curl -fsS https://api.certificate.vitni.cc/api/v1/healthcurl -fsS https://<instance>.zitadel.cloud/.well-known/openid-configurationOpen the site and test this sequence with a fresh account: sign in, create an organization, configure its issuer, author a badge, issue a credential, and verify the public credential. Enable email only after Resend sender verification; then issue one test credential to confirm claim-email delivery.
Check the docs at https://docs.certificate.vitni.cc and API contract at https://api.certificate.vitni.cc/api/v1/openapi.