Deployment
Minoro applications can be deployed to various platforms depending on your needs. Minoro supports both traditional server deployments and modern serverless architectures.
Deployment Examples (Non exhaustive)
Server with Docker (Recommended)
Docker deployment is ideal for traditional server environments and provides full control over your deployment stack. This method is recommended for production deployments due to its simplicity and reliability. You also get the benefit of running Minoro on your own hardware or cloud instances without vendor lock-in.
Setup: We recommend using Bun as the runtime to deploy Minoro for its performance, simplicity and first-class TS support.
Show me the code!
FROM oven/bun:1 as base
WORKDIR /app
COPY package.json ./
RUN bun install --production
COPY . ./
CMD ["bun", "run", "index.ts"]services:
minoro:
build:
context: .
dockerfile: Dockerfile
container_name: minoro
restart: unless-stopped
ports:
- "3000:3000"
environment:
- DATABASE_USER=${DATABASE_USER:-postgres}
- DATABASE_PASSWORD=${DATABASE_PASSWORD:-postgres}
- DATABASE_HOST=${DATABASE_HOST:-localhost}
- DATABASE_PORT=${DATABASE_PORT:-5432}
- DATABASE_NAME=${DATABASE_NAME:-minoro}
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}import { createMinoro, defineView } from 'minoro'
import { z } from 'zod'
const simpleView = defineView(
{ name: 'Simple view', icon: 'lucide:info' },
(view) => {
view.defineText({
text: 'This is a simple view with a button.',
})
view.defineButton({
label: 'Do something',
action: async () => {},
})
},
)
// we use zod to validate the environment variables
const env = z
.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.string().optional().default('5432'),
DATABASE_NAME: z.string(),
FRONTEND_URL: z.string(),
PORT: z.coerce.number().optional().default(3000),
})
.parse(process.env)
const minoro = createMinoro({
// Pass the view we want to use
views: [simpleView],
// The public URL or you admin UI e.g. https://example.com
allowedOrigins: [env.FRONTEND_URL],
// The admin token that will be used to create your first admin user
ONE_TIME_ADMIN_TOKEN: 'EXAMPLE',
// Connect to your database
async setup() {
return {
database: {
type: 'postgres',
connection: `postgres://${env.DATABASE_USER}:${env.DATABASE_PASSWORD}@${env.DATABASE_HOST}:${env.DATABASE_PORT}/${env.DATABASE_NAME}`,
},
}
},
})
await minoro.ready
// serve the ui directly, we could also choose to host it on a CDN or anywhere else
await minoro.serveStaticUi()
// Start!
Bun.serve({ fetch: minoro.fetch, port: env.PORT })Serverless with Netlify
Netlify deployment leverages serverless functions for the backend and static hosting for the UI.
Steps
- Create a
netlify.tomlfile to configure your Netlify deployment. - Create the
./netlify/functions/api.jsfile to handle the Minoro API. - Configure Netlify to run the
./build.tsscript before deployment. - Configure Netlify to serve
dist/uias the static site.
Show me the code!
// ./netlify/functions/api.js
// @ts-check
import { createMinoro, defineView } from 'minoro'
import { connectionString, env } from '../../env'
const simpleView = defineView(
{ name: 'Simple view', icon: 'lucide:info' },
(view) => {
view.defineText({
text: 'This is a simple view with a button.',
})
view.defineButton({
label: 'Do something',
action: async () => {},
})
},
)
/**
* @type {import("@netlify/functions").Config}
*/
export const config = {
path: ['/api/*'],
}
/**
* @param req {Request}
* @param ctx {import("@netlify/functions").Context}
* @returns {Promise<Response>}
*/
export default async function handler(req, ctx) {
const minoro = createMinoro({
views: [simpleView],
allowedOrigins: [env.PUBLIC_URL],
// read from environment variable
ONE_TIME_ADMIN_TOKEN: 'EXAMPLE',
async setup() {
return {
database: {
type: 'postgres',
connection: connectionString,
// migrations are run at build time
runMigrations: false,
},
}
},
})
// we ensure everything is ready before handling the request
await minoro.ready
return minoro.fetch(req)
}import fs from 'fs/promises'
import { findPublicUi, migrateDatabase } from 'minoro'
import { connectionString } from './env'
// find the static UI files generated by Minoro.
const publicPath = await findPublicUi()
// copy them into our app's dist/ui so that Netlify can serve them
await fs.mkdir('dist', { recursive: true })
await fs.cp(publicPath, 'dist/ui', {
recursive: true,
})
// the frontend is ready to be served but we need to run database migrations.
// your database should be set up and running before this script is run.
await migrateDatabase({
type: 'postgres',
connection: connectionString,
})
console.log('Database migrations completed successfully!')# Configure the Netlify deployment, including redirects and function settings. You should also configure Netlify to serve the dist/ui directory as the static site root.
[dev]
functions = "netlify/functions"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200// @ts-check
import { z } from 'zod'
const netlifyEnv = () => {
try {
// @ts-ignore
return Netlify.env.toObject()
} catch (error) {
return {}
}
}
export const env = z
.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.string().optional().default('5432'),
DATABASE_NAME: z.string(),
PUBLIC_URL: z.string(),
})
.parse({
...netlifyEnv(),
...process.env,
})
export const connectionString = `postgres://${env.DATABASE_USER}:${env.DATABASE_PASSWORD}@${env.DATABASE_HOST}:${env.DATABASE_PORT}/${env.DATABASE_NAME}`Serverless with Vercel
Vercel deployment uses serverless functions for the backend and static hosting for the UI.
Steps
- Create a
vercel.jsonfile to configure your Vercel deployment. - Create the
./api/index.jsfile to handle the Minoro API. - Configure Vercel to run the
./build.tsscript before deployment. - Configure Vercel to serve
dist/uias the static site.
Show me the code!
// ./api/index.js
// @ts-check
import { createMinoro, defineView } from 'minoro'
import { env, connectionString } from '../env'
const simpleView = defineView(
{ name: 'Simple view', icon: 'lucide:info' },
(view) => {
view.defineText({
text: 'This is a simple view with a button.',
})
view.defineButton({
label: 'Do something',
action: async () => {},
})
},
)
/**
* @param req {Request}
* @returns {Promise<Response>}
*/
async function handler(req) {
const minoro = createMinoro({
views: [simpleView],
// your vercel domain
allowedOrigins: [env.PUBLIC_URL],
// read from environment variable
ONE_TIME_ADMIN_TOKEN: 'EXAMPLE',
async setup() {
return {
database: {
type: 'postgres',
connection: connectionString,
},
}
},
})
// we ensure everything is ready before handling the request
await minoro.ready
return minoro.fetch(req)
}
export const GET = handler
export const POST = handler
export const OPTIONS = handlerimport fs from 'fs/promises'
import { findPublicUi, migrateDatabase } from 'minoro'
import { connectionString } from './env'
// find the static UI files generated by Minoro.
const publicPath = await findPublicUi()
// copy them into our app's dist/ui so that Vercel can serve them
await fs.mkdir('dist', { recursive: true })
await fs.cp(publicPath, 'dist/ui', {
recursive: true,
})
// the frontend is ready to be served but we need to run database migrations.
// your database should be set up and running before this script is run.
await migrateDatabase({
type: 'postgres',
connection: connectionString,
})
console.log('Database migrations completed successfully!'){
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "bun run build.ts",
"outputDirectory": "dist/ui",
"rewrites": [{ "source": "/api/(.*)", "destination": "/api" }]
}// @ts-check
import { z } from 'zod'
export const env = z
.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.string().optional().default('5432'),
DATABASE_NAME: z.string(),
PUBLIC_URL: z.string(),
})
.parse(process.env)
export const connectionString = `postgres://${env.DATABASE_USER}:${env.DATABASE_PASSWORD}@${env.DATABASE_HOST}:${env.DATABASE_PORT}/${env.DATABASE_NAME}`