Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions app/(protected)/deeploys/project-draft/[projectHash]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import JobFormWrapper from '@components/create-job/JobFormWrapper';
import DraftOverview from '@components/draft/DraftOverview';
import StackManager from '@components/stack/StackManager';
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
import { routePath } from '@lib/routes/route-paths';
import db from '@lib/storage/db';
Expand All @@ -14,7 +15,7 @@ import { useParams, useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

export default function ProjectDraft() {
const { jobType, setJobType, setStep, projectPage, setProjectPage, pendingRecoveredJobPrefill } =
const { jobType, setJobType, isCreatingStack, setCreatingStack, setStep, projectPage, setProjectPage, pendingRecoveredJobPrefill } =
useDeploymentContext() as DeploymentContextType;

const router = useRouter();
Expand Down Expand Up @@ -42,15 +43,17 @@ export default function ProjectDraft() {

if (hasRecoveredPrefill && !hasAutoOpenedRecoveredPrefillRef.current) {
hasAutoOpenedRecoveredPrefillRef.current = true;
setCreatingStack(false);
setJobType(pendingRecoveredJobPrefill.jobType);
setStep(pendingRecoveredJobPrefill.jobType === JobType.Service ? 1 : 0);
return;
}

if (!hasRecoveredPrefill && !hasAutoOpenedRecoveredPrefillRef.current) {
setCreatingStack(false);
setJobType(undefined);
}
}, [pendingRecoveredJobPrefill, projectHash, setJobType, setProjectPage, setStep]);
}, [pendingRecoveredJobPrefill, projectHash, setCreatingStack, setJobType, setProjectPage, setStep]);

useEffect(() => {
suppressMissingProjectRedirectRef.current = false;
Expand Down Expand Up @@ -81,7 +84,7 @@ export default function ProjectDraft() {

const getProjectIdentity = () => <ProjectIdentity projectName={project.name} />;

return !jobType ? (
return !jobType && !isCreatingStack ? (
<>
{projectPage === ProjectPage.Payment ? (
<Payment
Expand All @@ -100,6 +103,7 @@ export default function ProjectDraft() {
project={project}
draftJobs={draftJobs}
projectIdentity={getProjectIdentity()}
projectHash={projectHash as string}
onBeforeDeleteProject={() => {
suppressMissingProjectRedirectRef.current = true;
}}
Expand All @@ -109,6 +113,13 @@ export default function ProjectDraft() {
/>
)}
</>
) : isCreatingStack ? (
<StackManager
projectHash={projectHash as string}
projectName={project.name}
mode="create"
onDone={() => setCreatingStack(false)}
/>
) : (
<JobFormWrapper projectName={project.name} draftJobsCount={draftJobs.length} />
);
Expand Down
26 changes: 23 additions & 3 deletions app/(protected)/deeploys/project/[projectHash]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import JobFormWrapper from '@components/create-job/JobFormWrapper';
import ProjectPageLoading from '@components/loading/ProjectPageLoading';
import ProjectOverview from '@components/project/ProjectOverview';
import StackManager from '@components/stack/StackManager';
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
import { routePath } from '@lib/routes/route-paths';
import db from '@lib/storage/db';
Expand All @@ -21,8 +22,17 @@ import { RiAlertLine } from 'react-icons/ri';
export default function Project() {
const router = useRouter();

const { jobType, setJobType, projectPage, setProjectPage, getProjectName, getRunningJobsWithDetails, hasEscrowPermission } =
useDeploymentContext() as DeploymentContextType;
const {
jobType,
setJobType,
isCreatingStack,
setCreatingStack,
projectPage,
setProjectPage,
getProjectName,
getRunningJobsWithDetails,
hasEscrowPermission,
} = useDeploymentContext() as DeploymentContextType;

const [isLoading, setLoading] = useState(true);
const [projectName, setProjectName] = useState<string | undefined>();
Expand All @@ -43,6 +53,7 @@ export default function Project() {
useEffect(() => {
setProjectPage(ProjectPage.Overview);
setJobType(undefined);
setCreatingStack(false);
}, []);

useEffect(() => {
Expand Down Expand Up @@ -110,7 +121,7 @@ export default function Project() {

const getProjectIdentity = () => <ProjectIdentity projectName={projectName} runtimeStatus={projectRuntimeStatus} />;

return !jobType ? (
return !jobType && !isCreatingStack ? (
<>
{projectPage === ProjectPage.Payment ? (
<Payment
Expand All @@ -128,12 +139,21 @@ export default function Project() {
runningJobs={runningJobsWithDetails}
draftJobs={draftJobs}
projectIdentity={getProjectIdentity()}
projectHash={projectHash}
fetchRunningJobs={fetchRunningJobs}
successfulJobs={successfulJobs}
setSuccessfulJobs={setSuccessfulJobs}
/>
)}
</>
) : isCreatingStack ? (
<StackManager
projectHash={projectHash as string}
projectName={projectName}
runningJobs={runningJobsWithDetails}
mode="create"
onDone={() => setCreatingStack(false)}
/>
) : (
<JobFormWrapper projectName={projectName} draftJobsCount={draftJobs.length} />
);
Expand Down
78 changes: 78 additions & 0 deletions docs/adr/adr-0001-stack-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# ADR-0001: Deeploy/Ratio1 Stack v1 (Multi-Service Apps)

- Status: Accepted
- Date: 2026-03-24
- Scope: `deeploy-dapp`, `edge_node`, `naeural_core`, `ratio1-sc`

## Context

Deeploy is currently job-centric: a project contains independent jobs, each with independent pricing/payment/lifecycle.

We need first-class support for multi-service applications (for example web/cms/db) while preserving:
- per-component billing and lifecycle as separate jobs,
- current contract and payment primitives where possible,
- existing single-job UX and API compatibility.

## Decision

### 1. Stack model

A **Stack** is a logical grouping of component jobs under one project.

- Stack = grouping + orchestration metadata.
- Stack Component = one deployable service definition.
- Each Stack Component compiles/deploys as one Ratio1 Job.

This keeps current payment/accounting aligned with on-chain job granularity.

### 2. v1 placement

v1 Stack placement mode is explicitly **`co-located`**.

- All components in a stack deployment target the same node.
- Distributed private stack networking is out of scope for v1.

### 3. Internal connectivity

v1 internal service discovery uses the existing semaphore/shared-memory mechanism.

- Provider components publish service connection data under deterministic stack-scoped keys.
- Consumer components wait for required providers and resolve env references from semaphore payloads.

No new overlay network, service mesh, or cross-node private DNS is introduced in v1.

### 4. Public ingress

Public exposure remains optional and per-component.

- Internal-only component: no public tunnel required.
- Public component: existing tunnel/public ingress path remains unchanged.

### 5. Contracts and billing

`ratio1-sc` remains unchanged for v1.

- Existing `createJobs` batch path already supports creating multiple independently billed jobs.
- Shared `projectHash` + off-chain metadata are sufficient for stack grouping/reconciliation in v1.

## Consequences

### Positive

- Backward-compatible with current job model and existing dapp flows.
- Minimal protocol risk for v1.
- Reuses runtime primitives already deployed in production.
- Clear path to richer v2 networking.

### Trade-offs

- v1 stacks are constrained to same-node placements.
- No first-class private cross-node routing.
- Internal discovery is key/value semaphore-based, not DNS-like service naming.

## v2 Deferred Items

- Cross-node private networking/overlay.
- Internal DNS/service aliases across nodes.
- Multi-node HA topologies for stack components.
- Protocol-level stack-native billing primitives.
63 changes: 63 additions & 0 deletions docs/stacks-v1-user-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Deeploy Stacks v1 User Guide

## What Is a Stack?

A **Stack** is a group of related application components under the same Deeploy project.

- Each component is deployed as a separate Ratio1 job.
- Billing remains per component/job.
- Stack metadata is used for grouping, dependency wiring, and co-located placement.

Example stack:
- `web` (public)
- `cms` (internal-only)
- `db` (internal-only)

## Exposure Modes

Each stack component chooses one mode:

- `internal-only`: no public tunnel is required.
- `public`: existing public ingress/tunnel behavior is enabled for that component.

In the example above, only `web` is public.

## Internal References

Component environment variables can reference upstream components:

- `ref(service.host)`
- `ref(service.port)`
- `ref(service.url)`
- `ref(service.container_ip)`

Examples:
- `DATABASE_HOST=ref(db.host)`
- `DATABASE_PORT=ref(db.port)`
- `DATABASE_URL=postgres://user:pass@ref(db.host):ref(db.port)/postgres`

For v1, these refs compile to runtime shared-memory/semaphore discovery values, not public URLs.

## v1 Placement Limitation

Stacks in v1 must use:

- placement mode: `co-located`
- target nodes: exactly one shared target node for all components

Distributed private networking across multiple nodes is deferred to v2.

## Deployment Flow

1. Open a project and create/edit a stack draft.
2. Configure component dependencies and refs.
3. Click `Prepare Deploy` to compile the stack into component draft jobs.
4. Continue through the existing `Payment` flow.
5. Deeploy creates one on-chain job per component in one batch.

## Compatibility

Existing single-job flows are unchanged:

- users can still create standalone job drafts as before,
- existing projects without stacks continue to work unchanged.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"test": "tsx --test src/lib/stacks/__tests__/*.test.ts",
"add-service": "tsx scripts/add-service.ts",
"validate-services": "tsx scripts/validate-services.ts"
},
Expand Down
5 changes: 5 additions & 0 deletions src/components/draft/DraftOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { InteractionContextType, useInteractionContext } from '@lib/contexts/interaction';
import { routePath } from '@lib/routes/route-paths';
import db from '@lib/storage/db';
import StackManager from '@components/stack/StackManager';
import ActionButton from '@shared/ActionButton';
import AddJobCard from '@shared/projects/AddJobCard';
import CancelButton from '@shared/projects/buttons/CancelButton';
Expand All @@ -21,12 +22,14 @@ export default function DraftOverview({
project,
draftJobs,
projectIdentity,
projectHash,
onBeforeDeleteProject,
onDeleteProjectFailed,
}: {
project: DraftProject;
draftJobs: DraftJob[] | undefined;
projectIdentity: React.ReactNode;
projectHash: string;
onBeforeDeleteProject?: () => void;
onDeleteProjectFailed?: () => void;
}) {
Expand Down Expand Up @@ -79,6 +82,8 @@ export default function DraftOverview({
{/* Add Job */}
<AddJobCard />

<StackManager projectHash={projectHash} mode="overview" />

{/* Jobs */}
{!!draftJobs && !!draftJobs.length && (
<>
Expand Down
5 changes: 5 additions & 0 deletions src/components/project/ProjectOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import GenericDraftJobsList from '@components/draft/job-lists/GenericDraftJobsList';
import NativeDraftJobsList from '@components/draft/job-lists/NativeDraftJobsList';
import ServiceDraftJobsList from '@components/draft/job-lists/ServiceDraftJobsList';
import StackManager from '@components/stack/StackManager';
import { getRunningJobResources, RunningJobResources } from '@data/containerResources';
import { DeploymentContextType, ProjectOverviewTab, useDeploymentContext } from '@lib/contexts/deployment';
import CustomTabs from '@shared/CustomTabs';
Expand All @@ -25,13 +26,15 @@ export default function ProjectOverview({
runningJobs,
draftJobs,
projectIdentity,
projectHash,
fetchRunningJobs,
successfulJobs,
setSuccessfulJobs,
}: {
runningJobs: RunningJobWithDetails[] | undefined;
draftJobs: DraftJob[] | undefined;
projectIdentity: React.ReactNode;
projectHash: string;
fetchRunningJobs: (appsOverride?: Apps) => Promise<void>;
successfulJobs: { text: string; serverAlias: string; tunnelURL?: string }[];
setSuccessfulJobs: (successfulJobs: { text: string; serverAlias: string; tunnelURL?: string }[]) => void;
Expand Down Expand Up @@ -91,6 +94,8 @@ export default function ProjectOverview({
{/* Add Job */}
<AddJobCard />

<StackManager projectHash={projectHash} runningJobs={runningJobs} mode="overview" />

{/* Jobs */}
<div className="mx-auto">
<CustomTabs
Expand Down
Loading
Loading