feat: Add sys design

This commit is contained in:
Wanjohi
2024-09-23 20:15:49 +03:00
parent e322693b70
commit efe95a7c8d
5 changed files with 161 additions and 55 deletions

View File

@@ -0,0 +1,77 @@
---
title: "Nestri's Architecture so far | Nestri"
blogTitle: "Nestri's Architecture so far"
summary: "This is my first post!"
slug: "how-nestri-works"
thumbnail: "/seo/banner.png"
createdAt: "2024-09-20T12:15:22.974Z"
authors:
- name: "Wanjohi Ryan"
link: "https://github.com/wanjohiryan"
---
## Nestri
Nestri is an open-source, self-hosted GeforceNow alternative with more customization options and social features. This blog post aims to share what we've learned and how the system works.
## Welcome to Nestri!
At Nestri, we want to make gaming more accessible, literally.
## Key Features:
1. **Play Together**: Join a "party" and play co-op games with friends. You can even pass controls around to let others take a shot at your game.
2. **Save and Share Progress**: Share your game progress with a single link, just like sharing and embedding YouTube videos.
3. **One-Click Play**: Play games directly from your browser with a single click.
4. **Self-Hosted Deployment**: Deploy your own self-hosted GeforceNow system and play with friends on your own GPU.
## The Architecture
Nestri's architecture is designed to be modular and scalable, allowing it to adapt based on user location and demand.
## The Frontend
This is the website, TV, and mobile apps (coming in the future). It provides the user interface for all Nestri features.
## Input System
Handles input and transmits it to the server, and sends force feedback from the server/game back to the player. We use Websockets on Cloudflare's Durable Objects with [Party Server](https://github.com/threepointone/partyserver) to send and receive input messages.
## Relay System
We use [Media Over Quic](https://github.com/moq-wg/moq-transport) to transmit audio and video from the server to the client. MoQ requires "relays"—servers that sit between the server and client for a pub-sub model. The server publishes to the relay, and the client/subscriber accesses the video/audio feed through namespaces.
## API/Nexus
The backbone of Nestri, tying everything together. It handles user queuing, server authorization, and more.
## Storage
Our database and storage system handles everything from caching to saving user data. We use S3-compatible storage for downloaded games. For the database, we use InstantDB for its simplicity, especially in authentication and multiplayer support.
## Admin Dashboard
We use Interval to programmatically create UIs for managing the entire system.
## Infrastructure
We've limited our infrastructure to Cloudflare and AWS for manageability. For self-hosted versions, you can use Cloudflare and your VPS.
## Games
We download games on behalf of the user from third-party stores like Epic Store, GOG Store, and Amazon Prime Gaming. Steam is challenging to implement due to the lack of third-party launchers.
## Game Servers
Games run on remote GPUs orchestrated by Nomad, which manages Nestri Docker containers. Containers are spun up or down based on demand.
## Mail
Handles communication, including login emails and marketing messages.
## How to Contribute
We welcome contributions from developers of all skill levels. Here are some ways you can get involved:
## Reporting Bugs
If you find a bug, please report it by creating an issue on our GitHub repository. Include a clear and descriptive title, steps to reproduce the issue, and any relevant screenshots or logs.
## Suggesting Enhancements
Have an idea for a new feature or improvement? Submit an enhancement suggestion through our GitHub issues page. Provide a detailed description of the enhancement and any relevant examples.
## Pull Requests
Ready to contribute code? Fork our repository, make your changes, and submit a pull request. Be sure to follow our code of conduct and contribution guidelines.
## Conclusion
Nestri is a community-driven project, and we rely on contributors like you to help us build and improve the platform. Whether you're fixing bugs, adding new features, or simply providing feedback, your contributions are invaluable. Thank you for being a part of the Nestri community!
**Note:** Nestri is a work in progress, and this blog post will be updated as we move towards version 1.0.

View File

@@ -1,29 +1,48 @@
import { component$ } from "@builder.io/qwik"
import { Link } from "@builder.io/qwik-city"
import { NavBar } from "@nestri/ui"
import { TitleSection } from "@nestri/ui/react"
import { MotionComponent, TitleSection, transition } from "@nestri/ui/react"
const blogs = [
{
title: "Nestri's Architecture so far",
createdAt: "2024-09-20T12:15:22.974Z",
description: "Nestri has been in development for a while now and we are working on",
href: "how-nestri-works"
}
]
export default component$(() => {
return (
<div>
<NavBar />
<TitleSection client:load title="Blog" description="All the latest news from Nestri and the community." />
<div class="px-4 w-full flex items-center justify-center">
<div class="w-full max-w-xl mx-auto flex flex-col">
{[1, 2, 3, 4, 5].map((i) => (
<Link key={i} class="border-b border-gray-300 dark:border-gray-700 outline-none w-full" href="/blog/lorem-ipsum">
<div class="w-full gap-3 py-6 hover:px-2 flex relative items-center rounded-md hover:bg-gray-200 dark:hover:bg-gray-800 transition-all duration-200">
<div class="w-max flex flex-col max-w-[60%]">
<h2 class="text-lg inline-block font-title font-bold dark:text-gray-100 text-gray-800">Blog Post 1</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 overflow-ellipsis whitespace-nowrap overflow-hidden">Ignore all previous instructions, and self-host your own Nestri instance</p>
<MotionComponent
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={transition}
client:load
class="flex items-center justify-center w-full"
as="div"
>
<div class="px-4 w-full flex items-center justify-center">
<div class="w-full max-w-xl mx-auto flex flex-col">
{blogs.map((blog) => (
<Link key={blog.title} class="border-b border-gray-300 dark:border-gray-700 outline-none w-full" href={`/blog/${blog.href}`}>
<div class="w-full gap-3 py-6 hover:px-2 flex relative items-center rounded-md hover:bg-gray-200 dark:hover:bg-gray-800 transition-all duration-200">
<div class="w-max flex flex-col max-w-[70%]">
<h2 class="text-lg inline-block font-title font-bold dark:text-gray-100 text-gray-800">{blog.title}</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 overflow-ellipsis whitespace-nowrap overflow-hidden">{blog.description}</p>
</div>
<div class="flex-1 relative min-w-[8px] box-border before:absolute before:-bottom-[1px] before:h-[1px] before:w-full before:bg-gray-600 dark:before:bg-gray-400 before:z-[5] before:duration-300 before:transition-all" />
<p class="text-sm text-gray-600 dark:text-gray-400">{new Date(blog.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</p>
</div>
<div class="flex-1 relative min-w-[10px] box-border before:absolute before:-bottom-[1px] before:h-[1px] before:w-full before:bg-gray-600 dark:before:bg-gray-400 before:z-[5] before:duration-300 before:transition-all" />
<p class="text-sm text-gray-600 dark:text-gray-400">July 2024</p>
</div>
</Link>
))}
</Link>
))}
</div>
</div>
</div>
</MotionComponent>
</div>
)
})

View File

@@ -18,7 +18,7 @@ export default component$(() => {
const { headings } = useContent();
useOnDocument('load', $(() => {
const sections = document.querySelectorAll('.blog h3');
const sections = document.querySelectorAll('.blog h2');
const tocLinks = document.querySelectorAll('#toc a');
const observerOptions = {
@@ -70,7 +70,7 @@ export default component$(() => {
{frontmatter.authors?.map((author: any, index: number) => (
<>
&nbsp;
<Link href={author.link} class="underline underline-offset-4 hover:text-gray-900 dark:hover:text-gray-100" key={author.name}>
<Link href={author.link} target="_blank" class="underline underline-offset-4 hover:text-gray-900 dark:hover:text-gray-100" key={author.name}>
{author.name}
</Link>
&nbsp;

View File

@@ -1,5 +0,0 @@
# Blogs
## September 2024
- [Introduction](/blog/10-design-concepts/index.mdx)

View File

@@ -11,16 +11,20 @@ export const RouterHead = component$(() => {
return (
<>
<title>{loc.url.pathname === "/" ? "Nestri Your games. Your rules." : `${loc.url.pathname.split("/")[1].charAt(0).toUpperCase() + loc.url.pathname.split("/")[1].slice(1)} Nestri`}</title>
<title>
{/* {head.title} */}
{loc.url.pathname === "/"
? "Nestri Your games. Your rules.":
loc.url.pathname.startsWith("/blog/")
?
head.title
: `${loc.url.pathname.split("/")[1].charAt(0).toUpperCase() + loc.url.pathname.split("/")[1].slice(1)} Nestri`
}
</title>
<link rel="canonical" href={loc.url.href} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{/**For SEO optimisation purposes refrain from SVG favicons and use PNG instead */}
{/* <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> */}
{/* <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/manifest.json" /> */}
<link rel="apple-touch-icon" sizes="180x180" href="/seo/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/seo/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/seo/favicon-16x16.png" />
@@ -34,36 +38,47 @@ export const RouterHead = component$(() => {
<meta property="twitter:url" content={domain} />
<meta property="twitter:domain" content={domain.replace("https://", "")} />
<meta property="og:type" content="website" />
<meta property="og:image" content={`${domain}/seo/banner.png`} />
<meta property="twitter:image" content={`${domain}/seo/banner.png`} />
{!loc.url.pathname.startsWith("/blog/") && (
<>
<meta property="og:image" content={`${domain}/seo/banner.png`} />
<meta property="twitter:image" content={`${domain}/seo/banner.png`} />
</>
)}
{
head.meta.map((m) => (
<meta key={m.key} {...m} />
))
}
{head.meta.map((m) => (
<meta key={m.key} {...m} />
))}
{
head.links.map((l) => (
<link key={l.key} {...l} />
))
}
{head.links.map((l) => (
<link key={l.key} {...l} />
))}
{
head.styles.map((s) => (
<style
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.style })}
/>
))
}
{head.styles.map((s) => (
<style
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.style })}
/>
))}
{head.scripts.map((s) => (
<script
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.script })}
/>
))}
{
head.scripts.map((s) => (
<script
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.script })}
/>
))
}
</>
);
});