GithubHelp home page GithubHelp logo

rphlmr / supa-fly-stack Goto Github PK

View Code? Open in Web Editor NEW
274.0 10.0 23.0 2.33 MB

The Remix Stack for deploying to Fly with Supabase, authentication, testing, linting, formatting, etc.

License: MIT License

Shell 0.34% JavaScript 6.37% Dockerfile 1.29% TypeScript 91.94% CSS 0.06%
remix-stack supabase flydotio remix-run

supa-fly-stack's Introduction

Remix Supa Fly Stack

This Readme will be re-written soon

The Remix Indie Stack

Learn more about Remix Stacks.

npx create-remix --template rphlmr/supa-fly-stack

What's in the stack

Not a fan of bits of the stack? Fork it, change it, and use npx create-remix --template your/repo! Make it your own.

Development

  • Create a Supabase Database (free tier gives you 2 databases)

    Note: Only one for playing around with Supabase or 2 for staging and production

    Note: Used all your free tiers ? Also works with Supabase CLI and local self-hosting

    Note: Create a strong database password, but prefer a passphrase, it'll be more easy to use in connection string (no need to escape special char)

    example : my_strong_passphrase

  • Go to https://app.supabase.io/project/{PROJECT}/settings/api to find your secrets

  • "Project API keys"

  • Add your SUPABASE_URL, SERVER_URL, SUPABASE_SERVICE_ROLE (aka service_role secret), SUPABASE_ANON_PUBLIC (aka anon public) and DATABASE_URL in the .env file

    Note: SERVER_URL is your localhost on dev. It'll work for magic link login

DATABASE_URL="postgres://postgres:{STAGING_POSTGRES_PASSWORD}@db.{STAGING_YOUR_INSTANCE_NAME}.supabase.co:5432/postgres"
SUPABASE_ANON_PUBLIC="{ANON_PUBLIC}"
SUPABASE_SERVICE_ROLE="{SERVICE_ROLE}"
SUPABASE_URL="https://{STAGING_YOUR_INSTANCE_NAME}.supabase.co"
SESSION_SECRET="super-duper-s3cret"
SERVER_URL="http://localhost:3000"
  • This step only applies if you've opted out of having the CLI install dependencies for you:

    npx remix init
  • Initial setup:

    npm run setup
  • Start dev server:

    npm run dev

This starts your app in development mode, rebuilding assets on file changes.

The database seed script creates a new user with some data you can use to get started:

Relevant code:

This is a pretty simple note-taking app, but it's a good example of how you can build a full-stack app with Prisma, Supabase, and Remix. The main functionality is creating users, logging in and out (handling access and refresh tokens + refresh on expiration), and creating and deleting notes.

Deployment

Do what you know if you are a Fly.io expert.

This Remix Stack comes with two GitHub Actions that handle automatically deploying your app to production and staging environments.

Prior to your first deployment, you'll need to do a few things:

  • Install Fly

  • Sign up and log in to Fly

    fly auth signup

    Note: If you have more than one Fly account, ensure that you are signed into the same account in the Fly CLI as you are in the browser. In your terminal, run fly auth whoami and ensure the email matches the Fly account signed into the browser.

  • Create two apps on Fly, one for staging and one for production:

    fly apps create supa-fly-stack-template
    fly apps create supa-fly-stack-template-staging  # ** not mandatory if you don't want a staging environnement **

    Note: For production app, make sure this name matches the app set in your fly.toml file. Otherwise, you will not be able to deploy.

    • Initialize Git.
    git init
  • Create a new GitHub Repository, and then add it as the remote for your project. Do not push your app yet!

    git remote add origin <ORIGIN_URL>
  • Add a FLY_API_TOKEN to your GitHub repo. To do this, go to your user settings on Fly and create a new token, then add it to your repo secrets with the name FLY_API_TOKEN.

  • Add a SESSION_SECRET, SUPABASE_URL, SUPABASE_SERVICE_ROLE,SUPABASE_ANON_PUBLIC, SERVER_URL and DATABASE_URL to your fly app secrets

    Note: To find your SERVER_URL, go to your fly.io dashboard

    To do this you can run the following commands:

    # production (--app name is resolved from fly.toml)
    fly secrets set SESSION_SECRET=$(openssl rand -hex 32)
    fly secrets set SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"
    fly secrets set SUPABASE_SERVICE_ROLE="{SUPABASE_SERVICE_ROLE}"
    fly secrets set SUPABASE_ANON_PUBLIC="{SUPABASE_ANON_PUBLIC}"
    fly secrets set DATABASE_URL="postgres://postgres:{POSTGRES_PASSWORD}@db.{YOUR_INSTANCE_NAME}.supabase.co:5432/postgres"
    fly secrets set SERVER_URL="https://{YOUR_STAGING_SERVEUR_URL}"
    
    # staging (specify --app name) ** not mandatory if you don't want a staging environnement **
    fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app supa-fly-stack-template-staging
    fly secrets set SUPABASE_URL="https://{YOUR_STAGING_INSTANCE_NAME}.supabase.co" --app supa-fly-stack-template-staging
    fly secrets set SUPABASE_SERVICE_ROLE="{STAGING_SUPABASE_SERVICE_ROLE}" --app supa-fly-stack-template-staging
    fly secrets set SUPABASE_ANON_PUBLIC="{STAGING_SUPABASE_ANON_PUBLIC}" --app supa-fly-stack-template-staging
    fly secrets set DATABASE_URL="postgres://postgres:{STAGING_POSTGRES_PASSWORD}@db.{STAGING_YOUR_INSTANCE_NAME}.supabase.co:5432/postgres" --app supa-fly-stack-template-staging
    fly secrets set SERVER_URL="https://{YOUR_STAGING_SERVEUR_URL}" --app supa-fly-stack-template-staging
    

    If you don't have openssl installed, you can also use 1password to generate a random secret, just replace $(openssl rand -hex 32) with the generated secret.

Now that everything is set up you can commit and push your changes to your repo. Every commit to your main branch will trigger a deployment to your production environment, and every commit to your dev branch will trigger a deployment to your staging environment.

Note: To deploy manually, just run fly deploy (It'll deploy app defined in fly.toml)

GitHub Actions

DISCLAIMER : Github actions ==> I'm not an expert about that. Read carefully before using it

We use GitHub Actions for continuous integration and deployment. Anything that gets into the main branch will be deployed to production after running tests/build/etc. Anything in the dev branch will be deployed to staging.

๐Ÿ‘‰ You have to add some env secrets for cypress. ๐Ÿ‘ˆ

Add a SESSION_SECRET, SUPABASE_URL, SUPABASE_SERVICE_ROLE,SUPABASE_ANON_PUBLIC, SERVER_URL and DATABASE_URL to your repo secrets

Testing

Cypress

We use Cypress for our End-to-End tests in this project. You'll find those in the cypress directory. As you make changes, add to an existing file or create a new file in the cypress/e2e directory to test your changes.

We use @testing-library/cypress for selecting elements on the page semantically.

To run these tests in development, complete your .env and run npm run test:e2e:dev which will start the dev server for the app as well as the Cypress client. Make sure the database is running in docker as described above.

We also have a utility to auto-delete the user at the end of your test. Just make sure to add this in each test file:

afterEach(() => {
	cy.cleanupUser();
});

That way, we can keep your test db clean and keep your tests isolated from one another.

Vitest

For lower level tests of utilities and individual components, we use vitest. We have DOM-specific assertion helpers via @testing-library/jest-dom.

Type Checking

This project uses TypeScript. It's recommended to get TypeScript set up for your editor to get a great in-editor experience with type checking and auto-complete. To run type checking across the whole project, run npm run typecheck.

Linting

This project uses ESLint for linting. That is configured in .eslintrc.js.

Formatting

We use Prettier for auto-formatting in this project. It's recommended to install an editor plugin (like the VSCode Prettier plugin) to get auto-formatting on save. There's also a npm run format script you can run to format all files in the project.

Start working with Supabase

You are now ready to go further, congrats!

To extend your Prisma schema and apply changes on your supabase database :

If your token expires in less than 1 hour (3600 seconds in Supabase Dashboard)

If you have a lower token lifetime than me (1 hour), you should take a look at REFRESH_ACCESS_TOKEN_THRESHOLD in ./app/modules/auth/session.server.ts and set what you think is the best value for your use case.

Supabase RLS

You may ask "can I use RLS with Remix".

The answer is "Yes" but It has a cost.

Using Supabase SDK server side to query your database (for those using RLS features) adds an extra delay due to calling a Gotrue rest API instead of directly calling the Postgres database (and this is fine because at first Supabase SDK is for those who don't have/want backend).

In my benchmark, it makes my pages twice slower. (~+200ms compared to a direct query with Prisma)

Supabase login with magic link

In order to make the register/login with magic link work, you will need to add some configuration to your Supabase. You need to add the site url as well as the redirect urls of your local, test and live app that will be used for oauth To do that navigate to Authentication > URL configiration and add the folowing values:

supa-fly-stack's People

Contributors

arfnds avatar cricrio avatar donkoko avatar michaeldeboey avatar micotodev avatar rphlmr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

supa-fly-stack's Issues

Question - JWT Token Expiring Error When Querying Supabase Directly

First off thank you for the application, I am using it as a reference for a solution I am creating.

I am running into a problem where I am getting an error saying

[1] { message: 'JWT expired', code: 'PGRST301', details: null, hint: null }

when doing queries in my application. I am confused because the call to getAuthSession is returning a valid session but when get the supabase client

    const supabaseClient = supabase(authSession?.accessToken as string);

and make a database call, it errors out

Possible issues with Cypress mocking?

Hi again, not sure if I'm doing anything wrong but when I run the headless cypress test, the mocking doesn't seem to work and it calls my Supabase API. I can see the users and notes being created and cleaned up in my Supabase dashboard during the tests.

And since it called the actual api, test cases were failing and I had to add this check to wait for the api finishes and route changed to make it pass.
Screen Shot 2022-07-23 at 20 41 52
smoke tests -- should allow you to make a note (failed)
smoke tests -- should allow you to register and login (failed)

Steps to reproduce:

  1. npx create-remix --template rphlmr/supa-fly-stack
  2. Correct the .env file
  3. npm run setup
  4. npm run test:e2e:run

Access token refresh not working

The refreshAccessToken function calls into supabase-js using

const { data, error } = await getSupabaseAdmin().auth.setSession({
access_token: "",
refresh_token: refreshToken,
});

(note access_token: "")
However, inside supabase we hit the code path

/**
   * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
   * If the refresh token or access token in the current session is invalid, an error will be thrown.
   * @param currentSession The current session that minimally contains an access token and refresh token.
   */
  async setSession(currentSession: {
    access_token: string
    refresh_token: string
  }): Promise<AuthResponse> {
    try {
      if (!currentSession.access_token || !currentSession.refresh_token) {
        throw new AuthSessionMissingError()
      }

So the code always errors (file can be found at node_modules/.pnpm/@[email protected]/node_modules/@supabase/gotrue-js/src/GoTrueClient.ts lines 626 and following)

V3 is coming

Discussed in #66

Originally posted by rphlmr February 17, 2023
Hello there,

I'll publish a v3 with some breaking changes on the "auth module".

I have launched a new stack with a new way to commit auth session and I want to have the same pattern on every stack I maintain :)

Nothing hard to understand but a major change: requireAuthSession will no more magically refresh the session in loader.
Currently:

  • in loader function, If the access_token expires, I refresh it and reload the loader.
  • in action function, because we can't reload it, you have to commit the session in every return json.

In future, you will have to commit the session in loader and action.
Because writing the full header is too long, I have imagined something and will provide a response helper to handle that for you.

export async function loader({ request }: LoaderArgs) {
  const authSession = await requireAuthSession(request); // maybe a refreshed authSession
  const { userId } = authSession;

  try {
    const notes = await getNotes({ userId });

    return response.ok(
      { notes },
      { authSession } // will commit it for you
    );
  } catch (cause) {
    throw response.error(cause, { authSession });
  }
}

export async function action({ request, params }: ActionArgs) {
  const authSession = await requireAuthSession(request); // maybe a refreshed authSession
  const { userId } = authSession;

  try {
    await deleteNote(params.id);

    return response.ok(
      { success: true },
      { authSession } // will commit it for you
    );
  } catch (cause) {
    return response.error(cause, { authSession });
  }
}

Spoilers: authSession will have a cookie property that is nothing more than the result of sessionStorage.commitSession(session, { maxAge: SESSION_MAX_AGE, }).
Then, response.ok or response.error will put this cookie in headers :)

You can check what is coming here: https://github.com/rphlmr/supa-stripe-stack

Be sure it works with Remix v2

I have to test that everything works with Remix v2.
Then, this stack will no longer received updates.
A better one will replace it ;)

LICENSE file

Just noticed CC BY-NC-SA 4.0 tacked on down the bottom of the README. A copyleft non-commercial license seems highly unusual for a template, however it's your project so absolutely at your discretion.

That said, could you please add a LICENSE file so that Github prominently displays the license?

SUPABASE_SERVICE_KEY needs to change into SUPABASE_SERVICE_ROLE in seed.ts

I guees this key was left out when changing in other places.

From:

if (!process.env.SUPABASE_SERVICE_KEY) {
  throw new Error("SUPABASE_SERVICE_KEY is not set");
}

const supabaseAdmin = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_KEY,
  { autoRefreshToken: false, persistSession: false }
);

To:

if (!process.env.SUPABASE_SERVICE_ROLE) {
  throw new Error("SUPABASE_SERVICE_ROLE is not set");
}

const supabaseAdmin = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE,
  { autoRefreshToken: false, persistSession: false }
);

Feature : replace hand-crafted auth things with Supabase Remix's package ?

Hello there,

Someone ask this and maybe it's a good idea to not scatter people with multiple ways to handle auth in Remix with Supabase.

Maybe I should rewrite auth with https://supabase.com/docs/guides/auth/auth-helpers/remix.

I don't want to replace Prisma ๐Ÿฅถ but only the Auth part.

After that, it should be a breeze to replace Prisma with Supabase to query your database if this It what you need for your project.

I'll probably do some abstraction to keep helpers like requireAuthSession and keep things simple to use in protected loaders/actions.

Is It something you are interested in or do you prefer to be able to modify what you want with the actual auth implementation?

Prisma 3.12.0 breaks seed.ts

Just a heads up I updated prisma and @prisma/client to 3.12.0 and began getting a

An error occured while running the seed command:
Error: Command was killed with SIGKILL (Forced termination): ts-node --require tsconfig-paths/register prisma/seed.ts

I downgraded back to 3.11.1 and everything worked as expected.

Error when running npm run setup (setup:seed)

This is what I get:

Running seed command `ts-node --require tsconfig-paths/register app/database/seed.server.ts` ...
Error: Could not create or get user
    at getUserId (/Users/kojo/kod/Egna/YouWish/youwish_app/app/database/seed.server.ts:35:9)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async seed (/Users/kojo/kod/Egna/YouWish/youwish_app/app/database/seed.server.ts:39:14)

An error occurred while running the seed command:
Error: Command failed with exit code 1: ts-node --require tsconfig-paths/register app/database/seed.server.ts
ERROR: "setup:seed" exited with 1.

Any clues?
I followed every step and created the shadow db as described in supabase docs

remix.init script failed

I tried to use the supa-fly-stack template but it failed with the message shown below.
I'm running on Windows 10
npx --version = 10.2.5
node --version = v20.10.0

> npx create-remix --template rphlmr/supa-fly-stack
 remix   v2.4.0 ๐Ÿ’ฟ Let's build a better website...

   dir   Where should we create your new project?
         ./appname

      โ—ผ  Template: Using rphlmr/supa-fly-stack...
      โœ”  Template copied

   git   Initialize a new git repository?
         Yes

  deps   Install dependencies with npm?
         Yes

  init   This template has a remix.init script. Do you want to run it?
         Yes

 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ  Dependencies installing with npm...

      โ–ฒ  Oh no! Failed to install dependencies.

if I cd into the app-dir and run npm install I get the following output

โ€บ npm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: appname@undefined
npm ERR! Found: @remix-run/[email protected]
npm ERR! node_modules/@remix-run/react
npm ERR!   @remix-run/react@"^2.4.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @remix-run/react@"^1.0.0" from [email protected]
npm ERR! node_modules/remix-i18next
npm ERR!   remix-i18next@"^4.1.1" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR!
npm ERR! For a full report see:
npm ERR! C:\Users\user.name\AppData\Local\npm-cache\_logs\2023-12-21T10_52_51_177Z-eresolve-report.txt

npm ERR! A complete log of this run can be found in: C:\Users\user.name\AppData\Local\npm-cache\_logs\2023-12-21T10_52_51_177Z-debug-0.log

Error installing dependences on Ubuntu 22.04

Here's the output from npm install

npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: shelf@undefined npm ERR! Found: @remix-run/[email protected] npm ERR! node_modules/@remix-run/react npm ERR! @remix-run/react@"^2.3.0" from the root project npm ERR! npm ERR! Could not resolve dependency: npm ERR! peer @remix-run/react@"^1.0.0" from [email protected] npm ERR! node_modules/remix-i18next npm ERR! remix-i18next@"^4.1.1" from the root project npm ERR! npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution. npm ERR!

Debugging / Sourcemaps?

When debugging in vs code, breakpoints are shown in ./build/index.js and not in the original files. Is this expected / Is there a way to get full source map support?

Question: Magic link is not working as expected

We are having an issue with our magic link. I have tested this both in my localhost as well as on a fly.io environment.

Some more context:

Localhost

1 .Front-end works good, it sends the info
2. Email is received
3. Clicking on the email link gives a problem:

Screenshot 2023-02-17 at 13 25 27

When I inspect the link that comes in the email, there is some strange stuff I notice:

redirect_to=shelf-webapp.fly.dev/
I dont understand why this is the redirect url. I have double checked and on my localhost the SERVER_URL is set to localhost:3000.
I also specifically debugged the function sendMagicLink and the SERVER_URL that is being sent is localhost:3000.

I am a bit lost and not sure why its not working.

Fly.io

Basically everything is the same like locahost except the url being wrong. The url in the link is correct, but we are still getting the same error in the browser, that no API key is found in the request.

Any help would be appreciated.

Realtime

I was having trouble getting realtime to work with RLS. I was able to get it working like this:

const getSupabaseClient = (supabaseKey: string, accessToken?: string) => {
  const global = accessToken
    ? {
        global: {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        },
      }
    : {};

  const client = createClient<Database>(SUPABASE_API_URL, supabaseKey, {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
    ...global,
  });

  if (accessToken) {
    client.realtime.accessToken = accessToken;
    client.realtime.headers = {
      Authorization: `Bearer ${accessToken}`,
    };
  }

  return client;
};

Without this, the realtime claims in the realtime.subscriptions table were equivalent to a user with "role":"anon"

Auth Oddities / Vulnerabilities

There's a (minor?) security issue and a few oddities I noticed with the way auth is being handled.

OAuth Callback Auth Session Vulnerability

The action in oauth.callback.tsx accepts a form data encoded AuthSession from the client and trusts its contents. Consequently, you can submit a real email with a fake access token etc. and side effects will be triggered.

If an account exists for the email, the server will commit (sign) the bogus auth session data and store it in a cookie used for future requests. If a user doesn't exist for the email, one will be created and we'll also end up with bogus auth session cookie as above.

Whilst not great, this is fairly safe at present since requireAuthSession() is being used on endpoints, and it calls through to verifyAuthSession(), and consequently it'll catch the bad access token. That does assume APIs are calling through to verifyAuthSession(). However, that leads into another concern, which is admittedly more of a query.

Why create our own auth session cookie at all?

The Supabase JS library is already creating its own sb-access-token cookie which contains all the same data. The main difference is it's created/verifiable with the Supabase instance's JWT secret key. Thus, in order to validate the token we need do one of:

  1. Perform a round trip to Supabase as verifyAuthSession() does.
  2. Grab the JWT secret from the Supabase dashboard and add it as a server-only environment variable. Then we can then validate the JWTs directly on our server without the round trip.

I guess the advantage of our own session cookie is that we can validate it ourselves on the server without needing to grab the Supabase JWT secret and save on a round trip i.e. we can trust its contents. However, we're not doing that, we are performing round trips via requireAuthSession(). Just as well because of the aforementioned OAuth Callback vulnerability. However, it does make the contents of the cookie redundant, since we can't trust the contents without verifying the access token, and in doing so, we're fetching the same data that's found in the cookie anyway.

Email Addresses as Unique Keys

Email addresses are being used as unique keys. This stack doesn't claim to support oauth beyond magic links, so it's fine. However, this approach becomes problematic when integrating social OAuth - which is what I was doing when I discovered all the above. Basically, users can change the email address associated with their OAuth provider. Say for example someone signs up via Github social auth whilst their email is [email protected]. If they proceed to change their primary email on Github to [email protected], when that user returns to a site built on this stack and attempts to authenticate, no account will be found.

Again, the stack doesn't claim to support social auth, so it's by no means a bug. However, that part of the auth design could probably be shored up a bit to make it more robust/extensible.

Thank you

After me ranting about problems, just wanted to say thanks for all the hard work! Despite the above, this is my first time using Remix and this repo is, at the very least, super informative.

Support supabase-js v2 & future of this stack

I'll start working on migrating this stack to supabase-js v2.
It's still a release candidate so it'll not be merged to master until it's stable ๐Ÿ˜‰
I think it's possible to create a remix project from a stack's branch name, so, you'll be able to test this in advance.

Maybe some changes will happen on main to prepare for this upgrade.

I plan to :

  • make a better readme
  • clean some messy things like rewriting RLS and real-time examples, extracting components, etc...
  • add some doc on how to delete what you don't want to start your new project

No release date, doing my best ๐Ÿ˜…

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.