Skip to content

Commit 8c3a868

Browse files
authored
Merge pull request #676 from objectstack-ai/copilot/add-demo-preview-mode
2 parents e31ad98 + 1909884 commit 8c3a868

6 files changed

Lines changed: 336 additions & 12 deletions

File tree

content/docs/references/kernel/context.mdx

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Defines the operating mode of the kernel
1414
## TypeScript Usage
1515

1616
```typescript
17-
import { KernelContext, RuntimeMode } from '@objectstack/spec/kernel';
18-
import type { KernelContext, RuntimeMode } from '@objectstack/spec/kernel';
17+
import { KernelContext, RuntimeMode, PreviewModeConfig } from '@objectstack/spec/kernel';
18+
import type { KernelContext, RuntimeMode, PreviewModeConfig } from '@objectstack/spec/kernel';
1919

2020
// Validate data
2121
const result = KernelContext.parse(data);
@@ -30,13 +30,14 @@ const result = KernelContext.parse(data);
3030
| Property | Type | Required | Description |
3131
| :--- | :--- | :--- | :--- |
3232
| **instanceId** | `string` || Unique UUID for this running kernel process |
33-
| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning'>` || Kernel operating mode |
33+
| **mode** | `Enum<'development' \| 'production' \| 'test' \| 'provisioning' \| 'preview'>` || Kernel operating mode |
3434
| **version** | `string` || Kernel version |
3535
| **appName** | `string` | optional | Host application name |
3636
| **cwd** | `string` || Current working directory |
3737
| **workspaceRoot** | `string` | optional | Workspace root if different from cwd |
3838
| **startTime** | `integer` || Boot timestamp (ms) |
3939
| **features** | `Record<string, boolean>` || Global feature toggles |
40+
| **previewMode** | `PreviewModeConfig` | optional | Preview/demo mode configuration (used when mode is `'preview'`) |
4041

4142

4243
---
@@ -47,11 +48,56 @@ Kernel operating mode
4748

4849
### Allowed Values
4950

50-
* `development`
51-
* `production`
52-
* `test`
53-
* `provisioning`
51+
* `development` — Hot-reload, verbose logging
52+
* `production` — Optimized, strict security
53+
* `test` — Mocked interfaces
54+
* `provisioning` — Setup/Migration mode
55+
* `preview` — Demo/preview mode — bypass auth, simulate admin identity
5456

5557

5658
---
5759

60+
## PreviewModeConfig
61+
62+
Configures the kernel's preview/demo mode behaviour. When `mode` is set to `'preview'`, the platform
63+
skips authentication screens and simulates an admin identity so visitors can explore the system without
64+
registering or logging in.
65+
66+
<Callout type="warn">
67+
**Security:** Preview mode should **never** be used in production environments.
68+
</Callout>
69+
70+
### Properties
71+
72+
| Property | Type | Default | Description |
73+
| :--- | :--- | :--- | :--- |
74+
| **autoLogin** | `boolean` | `true` | Auto-login as simulated user, skipping login/registration pages |
75+
| **simulatedRole** | `Enum<'admin' \| 'user' \| 'viewer'>` | `'admin'` | Permission role for the simulated preview user |
76+
| **simulatedUserName** | `string` | `'Preview User'` | Display name for the simulated preview user |
77+
| **readOnly** | `boolean` | `false` | Restrict the preview session to read-only operations |
78+
| **expiresInSeconds** | `integer` | `0` | Preview session duration in seconds (0 = no expiration) |
79+
| **bannerMessage** | `string` || Banner message displayed in the UI during preview mode |
80+
81+
### Example
82+
83+
```typescript
84+
import { KernelContextSchema } from '@objectstack/spec/kernel';
85+
86+
const ctx = KernelContextSchema.parse({
87+
instanceId: '550e8400-e29b-41d4-a716-446655440000',
88+
mode: 'preview',
89+
version: '1.0.0',
90+
cwd: '/app',
91+
startTime: Date.now(),
92+
previewMode: {
93+
autoLogin: true,
94+
simulatedRole: 'admin',
95+
simulatedUserName: 'Demo Admin',
96+
readOnly: false,
97+
bannerMessage: 'You are exploring a demo — data will be reset periodically.',
98+
},
99+
});
100+
```
101+
102+
---
103+

examples/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,18 @@ pnpm build
152152
**Level:** 🔴 Advanced
153153
**Protocols:** System, API, Data
154154

155-
**Complete server implementation** showing how to build a metadata-driven backend. Features dynamic schema loading from plugins, auto-generated REST APIs, unified metadata API, and plugin orchestration.
155+
**Complete server implementation** showing how to build a metadata-driven backend. Features dynamic schema loading from plugins, auto-generated REST APIs, unified metadata API, plugin orchestration, and **preview/demo mode**.
156+
157+
**Preview Mode:** Run with `OS_MODE=preview` to bypass login/registration and simulate an admin identity — ideal for marketplace demos and app showcases.
156158

157159
**Quick Start:**
158160
```bash
159161
cd examples/app-host
160162
pnpm install
161163
pnpm dev
162-
# API available at http://localhost:3000
164+
165+
# Preview mode (no login required)
166+
OS_MODE=preview pnpm dev
163167
```
164168

165169
---
@@ -225,6 +229,7 @@ pnpm typecheck
225229
|----------|---------|----------|
226230
| Manifest | ✅ Complete | All examples with `objectstack.config.ts` |
227231
| Plugin System | ✅ Complete | [App Host](./app-host/) |
232+
| Preview Mode | ✅ Complete | [App Host](./app-host/)`OS_MODE=preview` |
228233
| Datasources | 🟡 Partial | [App Host](./app-host/) |
229234
| I18n / Translations | ✅ Complete | [Todo Translations](./app-todo/src/translations/), [CRM Translations](./app-crm/src/translations/) |
230235
| Job Scheduling | 🔴 Missing | _Planned_ |

examples/app-host/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ It demonstrates how to build a metadata-driven backend that dynamically loads ob
99
- **Unified Metadata API**: `/api/v1/meta/objects`
1010
- **Unified Data API**: `/api/v1/data/:object` (CRUD)
1111
- **Zero-Code Backend**: No creating routes or controllers per object.
12+
- **Preview Mode**: Run in demo mode — bypass login, auto-simulate admin identity.
1213

1314
## Setup
1415

@@ -28,6 +29,62 @@ It demonstrates how to build a metadata-driven backend that dynamically loads ob
2829
# Expected: Server starts at http://localhost:3000
2930
```
3031

32+
3. Run in **preview mode** (skip login, simulate admin):
33+
```bash
34+
OS_MODE=preview pnpm dev
35+
# Expected: Server starts in preview mode — no login required
36+
```
37+
38+
## Preview / Demo Mode
39+
40+
Preview mode allows visitors (e.g. marketplace customers) to explore the platform
41+
without registering or logging in. The kernel boots with `mode: 'preview'` and the
42+
frontend skips authentication screens, automatically simulating an admin session.
43+
44+
### How It Works
45+
46+
1. The runtime reads `OS_MODE=preview` from the environment (or the stack config).
47+
2. The `KernelContext` is created with `mode: 'preview'` and a `previewMode` config.
48+
3. The frontend detects `mode === 'preview'` and:
49+
- Hides the login / registration pages.
50+
- Automatically creates a simulated admin session.
51+
- Shows a preview banner to indicate demo mode.
52+
53+
### Configuration
54+
55+
```typescript
56+
import { KernelContextSchema } from '@objectstack/spec/kernel';
57+
58+
const ctx = KernelContextSchema.parse({
59+
instanceId: '550e8400-e29b-41d4-a716-446655440000',
60+
mode: 'preview',
61+
version: '1.0.0',
62+
cwd: process.cwd(),
63+
startTime: Date.now(),
64+
previewMode: {
65+
autoLogin: true, // Skip login/registration pages
66+
simulatedRole: 'admin', // Simulated user role (admin | user | viewer)
67+
simulatedUserName: 'Demo Admin',
68+
readOnly: false, // Allow writes (set true for read-only demos)
69+
expiresInSeconds: 3600, // Session expires after 1 hour (0 = no expiration)
70+
bannerMessage: 'You are exploring a demo — data will be reset periodically.',
71+
},
72+
});
73+
```
74+
75+
### PreviewModeConfig Properties
76+
77+
| Property | Type | Default | Description |
78+
|:---|:---|:---|:---|
79+
| **autoLogin** | `boolean` | `true` | Auto-login as simulated user, skip login/registration |
80+
| **simulatedRole** | `'admin' \| 'user' \| 'viewer'` | `'admin'` | Permission role for the simulated user |
81+
| **simulatedUserName** | `string` | `'Preview User'` | Display name shown in the UI |
82+
| **readOnly** | `boolean` | `false` | Block all write operations |
83+
| **expiresInSeconds** | `integer` | `0` | Session duration (0 = no expiration) |
84+
| **bannerMessage** | `string` || Banner message displayed in the UI |
85+
86+
> **⚠️ Security:** Preview mode should NEVER be used in production environments.
87+
3188
## API Usage Examples
3289

3390
### 1. Get All Objects

examples/app-host/objectstack.config.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,65 @@ export default defineStack({
3333
new AppPlugin(BiPluginManifest)
3434
]
3535
});
36+
37+
/**
38+
* Preview Mode Host Example
39+
*
40+
* Demonstrates how to run the platform in "preview" mode.
41+
* When `mode` is set to `'preview'`, the kernel signals the frontend to:
42+
* - Skip login/registration screens
43+
* - Automatically simulate an admin identity
44+
* - Display a preview-mode banner to the user
45+
*
46+
* Use this for marketplace demos, app showcases, or onboarding
47+
* tours where visitors should explore the system without signing up.
48+
*
49+
* ## Usage
50+
*
51+
* Set the `OS_MODE` environment variable to `preview` at boot:
52+
*
53+
* ```bash
54+
* OS_MODE=preview pnpm dev
55+
* ```
56+
*
57+
* Or use this stack definition directly as a starting point.
58+
*
59+
* ## KernelContext (created by the Runtime at boot)
60+
*
61+
* ```ts
62+
* import { KernelContextSchema } from '@objectstack/spec/kernel';
63+
*
64+
* const ctx = KernelContextSchema.parse({
65+
* instanceId: '550e8400-e29b-41d4-a716-446655440000',
66+
* mode: 'preview',
67+
* version: '1.0.0',
68+
* cwd: process.cwd(),
69+
* startTime: Date.now(),
70+
* previewMode: {
71+
* autoLogin: true,
72+
* simulatedRole: 'admin',
73+
* simulatedUserName: 'Demo Admin',
74+
* readOnly: false,
75+
* bannerMessage: 'You are exploring a demo — data will be reset periodically.',
76+
* },
77+
* });
78+
* ```
79+
*/
80+
export const PreviewHostExample = defineStack({
81+
manifest: {
82+
id: 'app-host-preview',
83+
name: 'app_host_preview',
84+
version: '1.0.0',
85+
description: 'Host application in preview/demo mode — bypasses login, simulates admin user',
86+
type: 'app',
87+
},
88+
89+
// Same plugins as the standard host
90+
plugins: [
91+
new ObjectQLPlugin(),
92+
new DriverPlugin(new InMemoryDriver()),
93+
new AppPlugin(CrmApp),
94+
new AppPlugin(TodoApp),
95+
new AppPlugin(BiPluginManifest)
96+
]
97+
});

packages/spec/src/kernel/context.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
22
import {
33
RuntimeMode,
44
KernelContextSchema,
5+
PreviewModeConfigSchema,
56
type KernelContext,
67
} from './context.zod';
78

@@ -11,6 +12,7 @@ describe('RuntimeMode', () => {
1112
expect(() => RuntimeMode.parse('production')).not.toThrow();
1213
expect(() => RuntimeMode.parse('test')).not.toThrow();
1314
expect(() => RuntimeMode.parse('provisioning')).not.toThrow();
15+
expect(() => RuntimeMode.parse('preview')).not.toThrow();
1416
});
1517

1618
it('should reject invalid runtime modes', () => {
@@ -87,10 +89,81 @@ describe('KernelContextSchema', () => {
8789
});
8890

8991
it('should accept all runtime modes in context', () => {
90-
const modes = ['development', 'production', 'test', 'provisioning'] as const;
92+
const modes = ['development', 'production', 'test', 'provisioning', 'preview'] as const;
9193
modes.forEach(mode => {
9294
const parsed = KernelContextSchema.parse({ ...validContext, mode });
9395
expect(parsed.mode).toBe(mode);
9496
});
9597
});
98+
99+
it('should accept preview mode with previewMode config', () => {
100+
const parsed = KernelContextSchema.parse({
101+
...validContext,
102+
mode: 'preview',
103+
previewMode: {
104+
autoLogin: true,
105+
simulatedRole: 'admin',
106+
simulatedUserName: 'Demo Admin',
107+
readOnly: true,
108+
expiresInSeconds: 3600,
109+
bannerMessage: 'You are viewing a demo of this application.',
110+
},
111+
});
112+
expect(parsed.mode).toBe('preview');
113+
expect(parsed.previewMode?.autoLogin).toBe(true);
114+
expect(parsed.previewMode?.simulatedRole).toBe('admin');
115+
expect(parsed.previewMode?.simulatedUserName).toBe('Demo Admin');
116+
expect(parsed.previewMode?.readOnly).toBe(true);
117+
expect(parsed.previewMode?.expiresInSeconds).toBe(3600);
118+
expect(parsed.previewMode?.bannerMessage).toContain('demo');
119+
});
120+
121+
it('should accept context without previewMode (optional)', () => {
122+
const parsed = KernelContextSchema.parse(validContext);
123+
expect(parsed.previewMode).toBeUndefined();
124+
});
125+
});
126+
127+
describe('PreviewModeConfigSchema', () => {
128+
it('should apply defaults for zero-config preview', () => {
129+
const parsed = PreviewModeConfigSchema.parse({});
130+
expect(parsed.autoLogin).toBe(true);
131+
expect(parsed.simulatedRole).toBe('admin');
132+
expect(parsed.simulatedUserName).toBe('Preview User');
133+
expect(parsed.readOnly).toBe(false);
134+
expect(parsed.expiresInSeconds).toBe(0);
135+
expect(parsed.bannerMessage).toBeUndefined();
136+
});
137+
138+
it('should accept all simulated roles', () => {
139+
const roles = ['admin', 'user', 'viewer'] as const;
140+
roles.forEach(role => {
141+
const parsed = PreviewModeConfigSchema.parse({ simulatedRole: role });
142+
expect(parsed.simulatedRole).toBe(role);
143+
});
144+
});
145+
146+
it('should reject invalid simulated role', () => {
147+
expect(() => PreviewModeConfigSchema.parse({ simulatedRole: 'superadmin' })).toThrow();
148+
});
149+
150+
it('should accept read-only preview for marketplace demos', () => {
151+
const parsed = PreviewModeConfigSchema.parse({
152+
autoLogin: true,
153+
simulatedRole: 'viewer',
154+
readOnly: true,
155+
bannerMessage: 'This is a preview. Sign up to get started!',
156+
});
157+
expect(parsed.readOnly).toBe(true);
158+
expect(parsed.simulatedRole).toBe('viewer');
159+
expect(parsed.bannerMessage).toContain('preview');
160+
});
161+
162+
it('should reject negative expiresInSeconds', () => {
163+
expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: -1 })).toThrow();
164+
});
165+
166+
it('should reject non-integer expiresInSeconds', () => {
167+
expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: 1.5 })).toThrow();
168+
});
96169
});

0 commit comments

Comments
 (0)