diff --git a/api/games/games.js b/api/games/games.js
deleted file mode 100644
index 8fe11b68a24f4e6c2b4def58882eda3410be2b00..0000000000000000000000000000000000000000
--- a/api/games/games.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import { Mongo } from 'meteor/mongo'
-import SimpleSchema from 'simpl-schema'
-import { ValidatedMethod } from 'meteor/mdg:validated-method'
-import { loggedInMixin, schemaMixin } from '../mixins'
-import { Meteor } from 'meteor/meteor'
-import { Card } from './cards'
-import { Bet, GameSystem } from './logic'
-
-export const Games = new Mongo.Collection('games')
-
-export const notInGameMixin = method => {
-  let run = method.run
-  method.run = function () {
-    if (Games.find({'players.id': this.userId}).count() > 0) throw new Meteor.Error('already_in_game')
-    return run.call(this, ...arguments)
-  }
-  return method
-}
-
-export const inGameMixin = method => {
-  let run = method.run
-  method.run = function () {
-    this.game = Games.findOne({'players.id': this.userId})
-    if (this.game == null) throw new Meteor.Error('not_in_game')
-    this.game.sys = GameSystem.fromObject(this.game.sys)
-    return run.call(this, ...arguments)
-  }
-  return method
-}
-
-const PlayerSchema = new SimpleSchema({
-  id: String,
-  position: {type: Number, allowedValues: [0, 1, 2, 3]}
-})
-
-const GameSchema = new SimpleSchema({
-  name: String,
-  players: {type: Array, defaultValue: [], optional: true},
-  'players.$': PlayerSchema,
-  sys: {type: GameSystem, defaultValue: new GameSystem(), optional: true}
-})
-
-export const newGame = new ValidatedMethod({
-  name: 'Game.new',
-  mixins: [schemaMixin, loggedInMixin, notInGameMixin],
-  validate: new SimpleSchema({
-    name: String
-  }),
-  run (game) {
-    if (Games.find(game).count() === 0) {
-      game = GameSchema.clean(game)
-      game.sys = game.sys.toObject()
-      return Games.insert(game)
-    } else {
-      throw new Meteor.Error('name_taken')
-    }
-  }
-})
-
-export const joinGame = new ValidatedMethod({
-  name: 'Game.join',
-  mixins: [schemaMixin, loggedInMixin, notInGameMixin],
-  validate: new SimpleSchema({
-    _id: String
-  }),
-  run ({_id}) {
-    let game = Games.findOne({_id})
-    if (game == null) throw new Meteor.Error('game_not_found')
-    if (game.players.length === 4) throw new Meteor.Error('game_full')
-    game.players.push({id: this.userId, position: game.players.length})
-    if (game.players.length === 4) {
-      game.sys = GameSystem.fromObject(game.sys)
-      game.sys.newRound()
-      game.sys = game.sys.toObject()
-    }
-    Games.update({_id}, {$set: game})
-  }
-})
-
-export const nextCards = new ValidatedMethod({
-  name: 'Game.nextCards',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({}),
-  run () {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-    if (!this.game.sys.nextCards(player)) {
-      throw new Meteor.Error('next_cards')
-    }
-    this.game.sys = this.game.sys.toObject()
-    Games.update({_id: this.game._id}, {$set: this.game})
-  }
-})
-
-export const giveCards = new ValidatedMethod({
-  name: 'Game.giveCards',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({
-    cards: {type: Array, minCount: 3, maxCount: 3},
-    'cards.$': Card
-  }),
-  run ({cards}) {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-    if (!this.game.sys.giveCards(player, cards)) {
-      throw new Meteor.Error('give_cards')
-    }
-    this.game.sys = this.game.sys.toObject()
-    Games.update({_id: this.game._id}, {$set: this.game})
-  }
-})
-
-export const playCards = new ValidatedMethod({
-  name: 'Game.playCards',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({
-    cards: [Card]
-  }),
-  run ({cards}) {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-
-    if (!this.game.sys.playCards(player, cards)) {
-      throw new Meteor.Error('play_cards')
-    }
-
-    let winner = this.game.sys.winner()
-
-    if (winner >= 0) {
-      Games.remove({_id: this.game._id})
-      return winner
-    } else {
-      this.game.sys = this.game.sys.toObject()
-      Games.update({_id: this.game._id}, {$set: this.game})
-    }
-  }
-})
-
-export const skipTurn = new ValidatedMethod({
-  name: 'Game.skipTurn',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({}),
-  run () {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-    if (!this.game.sys.skipTurn(player)) {
-      throw new Meteor.Error('skip_turn')
-    }
-    this.game.sys = this.game.sys.toObject()
-    Games.update({_id: this.game._id}, {$set: this.game})
-  }
-})
-
-export const giveDragon = new ValidatedMethod({
-  name: 'game.giveDragon',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({
-    to: {type: Number, allowedValues: [0, 1]}
-  }),
-  run ({to}) {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-    if (!this.game.sys.giveDragon(player, to)) {
-      throw new Meteor.Error('give_dragon')
-    }
-    this.game.sys = this.game.sys.toObject()
-    Games.update({_id: this.game._id}, {$set: this.game})
-  }
-})
-
-export const makeCall = new ValidatedMethod({
-  name: 'Game.makeCall',
-  mixins: [schemaMixin, loggedInMixin, inGameMixin],
-  validate: new SimpleSchema({
-    bet: {type: Number, allowedValues: [Bet.TICHU, Bet.GRAND]}
-  }),
-  run ({bet}) {
-    let player = this.game.players.findIndex(p => p.id === this.userId)
-    if (!this.game.sys.makeCall(player, bet)) {
-      throw new Meteor.Error('make_call')
-    }
-    this.game.sys = this.game.sys.toObject()
-    Games.update({_id: this.game._id}, {$set: this.game})
-  }
-})
diff --git a/package-lock.json b/package-lock.json
index d33ca0c14d7b685f0a360a27c5f5e7e22ce5b62a..a4841c2748a3c78fcccac0fd883cd1994adabba6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -187,6 +187,16 @@
         "@types/react": "*"
       }
     },
+    "@types/react-router-dom": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.1.tgz",
+      "integrity": "sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A==",
+      "requires": {
+        "@types/history": "*",
+        "@types/react": "*",
+        "@types/react-router": "*"
+      }
+    },
     "@types/underscore": {
       "version": "1.8.9",
       "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.8.9.tgz",
diff --git a/package.json b/package.json
index 03c97a29023458b87118ba2f9b91d9b66600c771..37d7a18a863ba82c75de58d2eeb502fc948ede78 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "@types/meteor": "^1.4.22",
     "@types/react-dom": "^16.0.9",
     "@types/react-router": "^4.0.32",
+    "@types/react-router-dom": "^4.3.1",
     "bcrypt": "^3.0.1",
     "chai": "^4.2.0",
     "collections": "^5.1.5",
diff --git a/startup/routes.tsx b/startup/routes.tsx
index e9b5d06c89b0cf9a3b3ac3e178a7d3e8f025b3a7..f1f8e016490188a9b02c090e7623cc6c0a34099c 100644
--- a/startup/routes.tsx
+++ b/startup/routes.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
 import { Route, Router, Switch } from 'react-router'
 import createBrowserHistory from 'history/createBrowserHistory'
 
-import GameListPage from '../ui/pages/game_list.jsx'
+import GameListPage from '../ui/pages/game_list'
 import GamePage from '../ui/pages/game.jsx'
 import IndexPage from '../ui/pages/index'
 
diff --git a/tsconfig.json b/tsconfig.json
index f09efd40e3f318645ae8e46dc276d0059c35cdc9..80d7d97d5404f03461a434225a2e02a60b7a96e6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,6 @@
 {
   "compilerOptions": {
     "target": "es5",
-    "module": "commonjs",
     "removeComments": true,
     "sourceMap": true,
     "downlevelIteration": true,
@@ -23,7 +22,8 @@
     "server/*",
     "api/**/*",
     "startup/**/*",
-    "ui/**/*"
+    "ui/**/*",
+    "typings/*"
   ],
   "exclude": [
     "node_modules",
diff --git a/typings/globals.d.ts b/typings/globals.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ed1c1bb3b0dfa48a566379ea9cb83c4dbe826b50
--- /dev/null
+++ b/typings/globals.d.ts
@@ -0,0 +1,7 @@
+interface Function {
+  name: string
+}
+
+interface Array<T> {
+  findIndex (predicate: (value: T, index: number, obj: Array<T>) => boolean, thisArg?: any): number
+}
diff --git a/typings/mdg_validated-method.d.ts b/typings/mdg_validated-method.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c3cbe6d1506689f283acade3cb50688a3cab108a
--- /dev/null
+++ b/typings/mdg_validated-method.d.ts
@@ -0,0 +1,22 @@
+declare module 'meteor/mdg:validated-method' {
+  class ValidatedMethod<A, R> extends MeteorValidatedMethod.ValidatedMethod<A, R> {}
+
+  module MeteorValidatedMethod {
+    export class ValidatedMethod<A, R> {
+      constructor (options: ValidatedMethodOptions<A, R>);
+
+      call (options?: A, cb?: (err: Error, res: R) => void): void;
+
+      run (this: any, opts: A): R;
+    }
+
+    interface ValidatedMethodOptions<A, R> {
+      name: string;
+      mixins?: ((method: ValidatedMethod<A, R>) => ValidatedMethod<A, R>)[];
+      validate: any;
+      applyOptions?: any;
+
+      run (this: any, opts: A): R;
+    }
+  }
+}
\ No newline at end of file
diff --git a/typings/react-meteor-data.d.ts b/typings/react-meteor-data.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ef495556f398c215b519fdad87f1e23cd4aa3c28
--- /dev/null
+++ b/typings/react-meteor-data.d.ts
@@ -0,0 +1,5 @@
+declare module 'meteor/react-meteor-data' {
+  function withTracker<T> (
+    getProps: () => T,
+  ): (component: React.ComponentClass<T> | React.StatelessComponent<T>) => React.ComponentClass<T>;
+}
diff --git a/typings/simpl-schema.d.ts b/typings/simpl-schema.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2895b48d15dca896a5b776b59e113cc81311492e
--- /dev/null
+++ b/typings/simpl-schema.d.ts
@@ -0,0 +1,166 @@
+declare module 'simpl-schema' {
+  /*
+    class ValidationContext {
+      constructor (ss: any);
+
+      addValidationErrors (errors: any): void;
+
+      clean (...args: any[]): any;
+
+      getErrorForKey (key: any, ...args: any[]): any;
+
+      isValid (): any;
+
+      keyErrorMessage (key: any, ...args: any[]): any;
+
+      keyIsInvalid (key: any, ...args: any[]): any;
+
+      reset (): void;
+
+      setValidationErrors (errors: any): void;
+
+      validate (obj: any, ...args: any[]): any;
+
+      validationErrors (): any;
+    }
+  // */
+  interface SchemaDefinition {
+    type: any;
+    label?: string | Function;
+    optional?: boolean | Function;
+    min?: number | boolean | Date | Function;
+    max?: number | boolean | Date | Function;
+    minCount?: number | Function;
+    maxCount?: number | Function;
+    allowedValues?: any[] | Function;
+    decimal?: boolean;
+    exclusiveMax?: boolean;
+    exclusiveMin?: boolean;
+    regEx?: RegExp | RegExp[];
+    custom?: Function;
+    blackbox?: boolean;
+    autoValue?: Function;
+    defaultValue?: any;
+    trim?: boolean;
+  }
+
+  interface CleanOption {
+    filter?: boolean;
+    autoConvert?: boolean;
+    removeEmptyStrings?: boolean;
+    trimStrings?: boolean;
+    getAutoValues?: boolean;
+    isModifier?: boolean;
+    extendAutoValueContext?: boolean;
+  }
+
+  class SimpleSchema {
+    constructor (schema: { [key: string]: SchemaDefinition | any } | any[]);
+
+    debug: boolean
+
+    namedContext (name?: string): SimpleSchemaValidationContextStatic;
+
+    addValidator (validator: Function): any;
+
+    pick (...fields: string[]): SimpleSchema;
+
+    omit (...fields: string[]): SimpleSchema;
+
+    clean (doc: any, options?: CleanOption): any;
+
+    schema (key?: string): SchemaDefinition | SchemaDefinition[];
+
+    getDefinition (key: string, propList?: any, functionContext?: any): any;
+
+    keyIsInBlackBox (key: string): boolean;
+
+    labels (labels: { [key: string]: string }): void;
+
+    label (key: any): any;
+
+    Integer: RegExp
+
+    messages (messages: any): void;
+
+    messageForError (type: any, key: any, def: any, value: any): string;
+
+    allowsKey (key: any): string;
+
+    newContext (): SimpleSchemaValidationContextStatic;
+
+    objectKeys (keyPrefix: any): any[];
+
+    validate (obj: any, options?: ValidationOption): void;
+
+    validator (options?: ValidationOption): Function;
+
+    RegEx: {
+      Email: RegExp;
+      EmailWithTLD: RegExp;
+      Domain: RegExp;
+      WeakDomain: RegExp;
+      IP: RegExp;
+      IPv4: RegExp;
+      IPv6: RegExp;
+      Url: RegExp;
+      Id: RegExp;
+      ZipCode: RegExp;
+      Phone: RegExp;
+    }
+  }
+
+  interface ValidationOption {
+    modifier?: boolean;
+    upsert?: boolean;
+    clean?: boolean;
+    filter?: boolean;
+    upsertextendedCustomContext?: boolean;
+  }
+
+  interface SimpleSchemaValidationContextStatic {
+    validate (obj: any, options?: ValidationOption): boolean;
+
+    validateOne (doc: any, keyName: string, options?: ValidationOption): boolean;
+
+    resetValidation (): void;
+
+    isValid (): boolean;
+
+    invalidKeys (): { name: string; type: string; value?: any; }[];
+
+    addInvalidKeys (errors: { name: string, type: string; }[]): void;
+
+    keyIsInvalid (name: any): boolean;
+
+    keyErrorMessage (name: any): string;
+
+    getErrorObject (): any;
+  }
+
+  /*
+  interface SimpleSchema {
+    debug: boolean;
+
+    addValidator (validator: Function): any;
+
+    extendOptions (options: { [key: string]: any }): void;
+
+    messages (messages: any): void;
+
+    RegEx: {
+      Email: RegExp;
+      Domain: RegExp;
+      WeakDomain: RegExp;
+      IP: RegExp;
+      IPv4: RegExp;
+      IPv6: RegExp;
+      Url: RegExp;
+      Id: RegExp;
+      ZipCode: RegExp;
+      Phone: RegExp;
+    };
+  }
+// */
+  export default SimpleSchema
+}
\ No newline at end of file
diff --git a/ui/components/game_entry.jsx b/ui/components/game_entry.tsx
similarity index 62%
rename from ui/components/game_entry.jsx
rename to ui/components/game_entry.tsx
index c5ac5c7535dd32bb89582301966990a94fc15f91..6311e9f7dfec03e7da03c64e1a38976c4ba78d42 100644
--- a/ui/components/game_entry.jsx
+++ b/ui/components/game_entry.tsx
@@ -1,7 +1,7 @@
-import React from 'react'
+import * as React from 'react'
 import { Link } from 'react-router-dom'
 
-export default class GameEntry extends React.Component {
+export default class GameEntry extends React.Component<{ game: {} }> {
   render () {
     return (
       <li><Link to={'/games/' + this.props.game._id}>{this.props.game.name}</Link></li>
diff --git a/ui/pages/game.jsx b/ui/pages/game.jsx
index 762a95e7d4f310518a2b8597f76aa962e56de90e..409233a5fe196a7c7909d04155d44b017e5b01e3 100644
--- a/ui/pages/game.jsx
+++ b/ui/pages/game.jsx
@@ -1,7 +1,7 @@
 import { Meteor } from 'meteor/meteor'
 import React from 'react'
 import { withTracker } from 'meteor/react-meteor-data'
-import { Games, joinGame } from '../../api/games/games.js'
+import { Games, joinGame } from '../../api/games/games.ts'
 import PlayerEntry from '../components/player_entry.jsx'
 import TichuGame from '../tichu_game'
 import { GameSystem } from '../../api/games/logic.js'
diff --git a/ui/pages/game_list.jsx b/ui/pages/game_list.tsx
similarity index 54%
rename from ui/pages/game_list.jsx
rename to ui/pages/game_list.tsx
index 7185e4baecec926a67c89a75d0aff462f5b38c3f..55877468f65d6cb9f11637731bf9e90ca5eebee8 100644
--- a/ui/pages/game_list.jsx
+++ b/ui/pages/game_list.tsx
@@ -1,14 +1,18 @@
-import React from 'react'
+import * as React from 'react'
 import { withTracker } from 'meteor/react-meteor-data'
-import GameEntry from '../components/game_entry.jsx'
-import { Games, newGame } from '../../api/games/games.js'
+import GameEntry from '../components/game_entry'
+import { Games, newGame } from '../../api/games/games'
 
-class GameList extends React.Component {
-  onSubmit (event) {
+interface SubmitEvent extends Event {
+  target: HTMLFormElement
+}
+
+class GameList extends React.Component<{ games: Array<{}> }> {
+  onSubmit (event: SubmitEvent) {
     event.preventDefault()
 
-    let name = event.target.name.value
-    newGame.call({name}, (err, res) => {
+    let name = event.target.tableName.value
+    newGame.call({name}, (err: Error) => {
       if (err) console.dir(err)
     })
   }
@@ -17,7 +21,7 @@ class GameList extends React.Component {
     return (
       <div>
         <form onSubmit={this.onSubmit.bind(this)}>
-          <input type="text" name="name" placeholder="Game name"/>
+          <input type="text" name="tableName" placeholder="Game name"/>
           <input type="submit" value="Create a new game"/>
         </form>
         <ul>